riemann-tools 1.1.1 → 1.4.0

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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +8 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +60 -2
  7. data/Rakefile +17 -3
  8. data/bin/riemann-apache-status +1 -106
  9. data/bin/riemann-bench +2 -70
  10. data/bin/riemann-cloudant +1 -56
  11. data/bin/riemann-consul +1 -106
  12. data/bin/riemann-dir-files-count +1 -55
  13. data/bin/riemann-dir-space +1 -55
  14. data/bin/riemann-diskstats +1 -92
  15. data/bin/riemann-fd +2 -81
  16. data/bin/riemann-freeswitch +2 -119
  17. data/bin/riemann-haproxy +1 -58
  18. data/bin/riemann-health +0 -2
  19. data/bin/riemann-kvminstance +2 -22
  20. data/bin/riemann-md +8 -0
  21. data/bin/riemann-memcached +1 -37
  22. data/bin/riemann-net +0 -2
  23. data/bin/riemann-nginx-status +1 -85
  24. data/bin/riemann-ntp +0 -2
  25. data/bin/riemann-portcheck +1 -44
  26. data/bin/riemann-proc +1 -108
  27. data/bin/riemann-varnish +1 -54
  28. data/bin/riemann-wrapper +113 -0
  29. data/bin/riemann-zookeeper +1 -39
  30. data/bin/riemann-zpool +8 -0
  31. data/lib/riemann/tools/apache_status.rb +107 -0
  32. data/lib/riemann/tools/bench.rb +72 -0
  33. data/lib/riemann/tools/cloudant.rb +57 -0
  34. data/lib/riemann/tools/consul_health.rb +107 -0
  35. data/lib/riemann/tools/dir_files_count.rb +56 -0
  36. data/lib/riemann/tools/dir_space.rb +56 -0
  37. data/lib/riemann/tools/diskstats.rb +94 -0
  38. data/lib/riemann/tools/fd.rb +81 -0
  39. data/lib/riemann/tools/freeswitch.rb +119 -0
  40. data/lib/riemann/tools/haproxy.rb +59 -0
  41. data/lib/riemann/tools/health.rb +150 -19
  42. data/lib/riemann/tools/kvm.rb +23 -0
  43. data/lib/riemann/tools/md.rb +35 -0
  44. data/lib/riemann/tools/mdstat_parser.tab.rb +340 -0
  45. data/lib/riemann/tools/memcached.rb +38 -0
  46. data/lib/riemann/tools/net.rb +2 -1
  47. data/lib/riemann/tools/nginx_status.rb +86 -0
  48. data/lib/riemann/tools/ntp.rb +1 -0
  49. data/lib/riemann/tools/portcheck.rb +45 -0
  50. data/lib/riemann/tools/proc.rb +109 -0
  51. data/lib/riemann/tools/riemann_client_wrapper.rb +43 -0
  52. data/lib/riemann/tools/uptime_parser.tab.rb +323 -0
  53. data/lib/riemann/tools/varnish.rb +55 -0
  54. data/lib/riemann/tools/version.rb +1 -1
  55. data/lib/riemann/tools/zookeeper.rb +40 -0
  56. data/lib/riemann/tools/zpool.rb +40 -0
  57. data/lib/riemann/tools.rb +2 -20
  58. data/riemann-tools.gemspec +10 -1
  59. data/tools/riemann-aws/Rakefile +6 -9
  60. data/tools/riemann-aws/bin/riemann-aws-billing +2 -87
  61. data/tools/riemann-aws/bin/riemann-aws-rds-status +2 -62
  62. data/tools/riemann-aws/bin/riemann-aws-sqs-status +2 -44
  63. data/tools/riemann-aws/bin/riemann-aws-status +2 -77
  64. data/tools/riemann-aws/bin/riemann-elb-metrics +2 -162
  65. data/tools/riemann-aws/bin/riemann-s3-list +2 -81
  66. data/tools/riemann-aws/bin/riemann-s3-status +2 -96
  67. data/tools/riemann-aws/lib/riemann/tools/aws/billing.rb +87 -0
  68. data/tools/riemann-aws/lib/riemann/tools/aws/elb_metrics.rb +163 -0
  69. data/tools/riemann-aws/lib/riemann/tools/aws/rds_status.rb +63 -0
  70. data/tools/riemann-aws/lib/riemann/tools/aws/s3_list.rb +82 -0
  71. data/tools/riemann-aws/lib/riemann/tools/aws/s3_status.rb +97 -0
  72. data/tools/riemann-aws/lib/riemann/tools/aws/sqs_status.rb +45 -0
  73. data/tools/riemann-aws/lib/riemann/tools/aws/status.rb +74 -0
  74. data/tools/riemann-chronos/Rakefile +6 -9
  75. data/tools/riemann-chronos/bin/riemann-chronos +1 -154
  76. data/tools/riemann-chronos/lib/riemann/tools/chronos.rb +157 -0
  77. data/tools/riemann-docker/Rakefile +5 -8
  78. data/tools/riemann-docker/bin/riemann-docker +2 -200
  79. data/tools/riemann-docker/lib/riemann/tools/docker.rb +200 -0
  80. data/tools/riemann-elasticsearch/Rakefile +6 -9
  81. data/tools/riemann-elasticsearch/bin/riemann-elasticsearch +1 -167
  82. data/tools/riemann-elasticsearch/lib/riemann/tools/elasticsearch.rb +170 -0
  83. data/tools/riemann-marathon/Rakefile +6 -9
  84. data/tools/riemann-marathon/bin/riemann-marathon +1 -156
  85. data/tools/riemann-marathon/lib/riemann/tools/marathon.rb +159 -0
  86. data/tools/riemann-mesos/Rakefile +6 -9
  87. data/tools/riemann-mesos/bin/riemann-mesos +1 -139
  88. data/tools/riemann-mesos/lib/riemann/tools/mesos.rb +142 -0
  89. data/tools/riemann-munin/Rakefile +5 -8
  90. data/tools/riemann-munin/bin/riemann-munin +1 -36
  91. data/tools/riemann-munin/lib/riemann/tools/munin.rb +37 -0
  92. data/tools/riemann-rabbitmq/Rakefile +6 -9
  93. data/tools/riemann-rabbitmq/bin/riemann-rabbitmq +1 -266
  94. data/tools/riemann-rabbitmq/lib/riemann/tools/rabbitmq.rb +269 -0
  95. data/tools/riemann-riak/Rakefile +5 -8
  96. data/tools/riemann-riak/bin/riemann-riak +1 -316
  97. data/tools/riemann-riak/bin/riemann-riak-keys +0 -1
  98. data/tools/riemann-riak/bin/riemann-riak-ring +0 -1
  99. data/tools/riemann-riak/lib/riemann/tools/riak.rb +317 -0
  100. metadata +64 -10
  101. data/.travis.yml +0 -31
  102. data/tools/riemann-riak/riak_status/key_count.erl +0 -13
  103. data/tools/riemann-riak/riak_status/riak_status.rb +0 -152
  104. data/tools/riemann-riak/riak_status/ringready.erl +0 -9
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'riemann/tools'
4
+
5
+ # Gets the number of files present on a directory and submits it to riemann
6
+ module Riemann
7
+ module Tools
8
+ class DirFilesCount
9
+ include Riemann::Tools
10
+
11
+ opt :directory, '', default: '/var/log'
12
+ opt :service_prefix, 'The first part of the service name, before the directory path', default: 'dir-files-count'
13
+ opt :warning, 'Dir files number warning threshold', type: Integer
14
+ opt :critical, 'Dir files number critical threshold', type: Integer
15
+ opt :alert_on_missing, 'Send a critical metric if the directory is missing?', default: true
16
+
17
+ def initialize
18
+ @dir = opts.fetch(:directory)
19
+ @service_prefix = opts.fetch(:service_prefix)
20
+ @warning = opts.fetch(:warning, nil)
21
+ @critical = opts.fetch(:critical, nil)
22
+ @alert_on_missing = opts.fetch(:alert_on_missing)
23
+ end
24
+
25
+ def tick
26
+ if Dir.exist?(@dir)
27
+ metric = Dir.entries(@dir).size - 2
28
+ report(
29
+ service: "#{@service_prefix} #{@dir}",
30
+ metric: metric,
31
+ state: state(metric),
32
+ tags: ['dir_files_count'],
33
+ )
34
+ elsif @alert_on_missing
35
+ report(
36
+ service: "#{@service_prefix} #{@dir} missing",
37
+ description: "#{@service_prefix} #{@dir} does not exist",
38
+ metric: metric,
39
+ state: 'critical',
40
+ tags: ['dir_files_count'],
41
+ )
42
+ end
43
+ end
44
+
45
+ def state(metric)
46
+ if @critical && metric > @critical
47
+ 'critical'
48
+ elsif @warning && metric > @warning
49
+ 'warning'
50
+ else
51
+ 'ok'
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'riemann/tools'
4
+
5
+ # Gathers the space used by a directory and submits it to riemann
6
+ module Riemann
7
+ module Tools
8
+ class DirSpace
9
+ include Riemann::Tools
10
+
11
+ opt :directory, '', default: '/var/log'
12
+ opt :service_prefix, 'The first part of the service name, before the directory path', default: 'dir-space'
13
+ opt :warning, 'Dir space warning threshold (in bytes)', type: Integer
14
+ opt :critical, 'Dir space critical threshold (in bytes)', type: Integer
15
+ opt :alert_on_missing, 'Send a critical metric if the directory is missing?', default: true
16
+
17
+ def initialize
18
+ @dir = opts.fetch(:directory)
19
+ @service_prefix = opts.fetch(:service_prefix)
20
+ @warning = opts.fetch(:warning, nil)
21
+ @critical = opts.fetch(:critical, nil)
22
+ @alert_on_missing = opts.fetch(:alert_on_missing)
23
+ end
24
+
25
+ def tick
26
+ if Dir.exist?(@dir)
27
+ metric = `du '#{@dir}'`.lines.to_a.last.split("\t")[0].to_i
28
+ report(
29
+ service: "#{@service_prefix} #{@dir}",
30
+ metric: metric,
31
+ state: state(metric),
32
+ tags: ['dir_space'],
33
+ )
34
+ elsif @alert_on_missing
35
+ report(
36
+ service: "#{@service_prefix} #{@dir} missing",
37
+ description: "#{@service_prefix} #{@dir} does not exist",
38
+ metric: metric,
39
+ state: 'critical',
40
+ tags: ['dir_space'],
41
+ )
42
+ end
43
+ end
44
+
45
+ def state(metric)
46
+ if @critical && metric > @critical
47
+ 'critical'
48
+ elsif @warning && metric > @warning
49
+ 'warning'
50
+ else
51
+ 'ok'
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'riemann/tools'
4
+
5
+ module Riemann
6
+ module Tools
7
+ class Diskstats
8
+ include Riemann::Tools
9
+
10
+ opt :devices, 'Devices to monitor', type: :strings, default: nil
11
+ opt :ignore_devices, 'Devices to ignore', type: :strings, default: nil
12
+
13
+ def initialize
14
+ @old_state = nil
15
+ end
16
+
17
+ def state
18
+ f = File.read('/proc/diskstats')
19
+ state = f.split("\n").reject { |d| d =~ /(ram|loop)/ }.each_with_object({}) do |line, s|
20
+ next unless line =~ /^(?:\s+\d+){2}\s+([\w\d\-]+) (.*)$/
21
+
22
+ dev = Regexp.last_match(1)
23
+
24
+ ['reads reqs',
25
+ 'reads merged',
26
+ 'reads sector',
27
+ 'reads time',
28
+ 'writes reqs',
29
+ 'writes merged',
30
+ 'writes sector',
31
+ 'writes time',
32
+ 'io reqs',
33
+ 'io time',
34
+ 'io weighted',].map do |service|
35
+ "#{dev} #{service}"
36
+ end.zip( # rubocop:disable Style/MultilineBlockChain
37
+ Regexp.last_match(2).split(/\s+/).map(&:to_i),
38
+ ).each do |service, value|
39
+ s[service] = value
40
+ end
41
+ end
42
+
43
+ # Filter interfaces
44
+ if (is = opts[:devices])
45
+ state = state.select do |service, _value|
46
+ is.include? service.split(' ').first
47
+ end
48
+ end
49
+
50
+ if (ign = opts[:ignore_devices])
51
+ state = state.reject do |service, _value|
52
+ ign.include? service.split(' ').first
53
+ end
54
+ end
55
+
56
+ state
57
+ end
58
+
59
+ def tick
60
+ state = self.state
61
+
62
+ if @old_state
63
+ state.each do |service, metric|
64
+ if service =~ /io reqs$/
65
+ report(
66
+ service: "diskstats #{service}",
67
+ metric: metric,
68
+ state: 'ok',
69
+ )
70
+ else
71
+ delta = metric - @old_state[service]
72
+
73
+ report(
74
+ service: "diskstats #{service}",
75
+ metric: (delta.to_f / opts[:interval]),
76
+ state: 'ok',
77
+ )
78
+ end
79
+
80
+ next unless service =~ /io time$/
81
+
82
+ report(
83
+ service: "diskstats #{service.gsub(/time/, 'util')}",
84
+ metric: (delta.to_f / (opts[:interval] * 1000)),
85
+ state: 'ok',
86
+ )
87
+ end
88
+ end
89
+
90
+ @old_state = state
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'riemann/tools'
4
+
5
+ # Reports current file descriptor use to riemann.
6
+ # By default reports the total system fd usage, can also report usage of individual processes
7
+ module Riemann
8
+ module Tools
9
+ class Fd
10
+ include Riemann::Tools
11
+
12
+ opt :fd_sys_warning, 'open file descriptor threshold for system', default: 800
13
+ opt :fd_sys_critical, 'open file descriptor critical threshold for system', default: 900
14
+ opt :fd_proc_warning, 'open file descriptor threshold for process', default: 800
15
+ opt :fd_proc_critical, 'open file descriptor critical threshold for process', default: 900
16
+ opt :processes, 'list of processes to measure fd usage in addition to system total', type: :ints
17
+
18
+ def initialize
19
+ @limits = {
20
+ fd: { critical: opts[:fd_sys_critical], warning: opts[:fd_sys_warning] },
21
+ process: { critical: opts[:fd_proc_critical], warning: opts[:fd_proc_warning] },
22
+ }
23
+ ostype = `uname -s`.chomp.downcase
24
+ case ostype
25
+ when 'freebsd'
26
+ @fd = method :freebsd_fd
27
+ else
28
+ puts "WARNING: OS '#{ostype}' not explicitly supported. Falling back to Linux" unless ostype == 'linux'
29
+ @fd = method :linux_fd
30
+ end
31
+ end
32
+
33
+ def alert(service, state, metric, description)
34
+ report(
35
+ service: service.to_s,
36
+ state: state.to_s,
37
+ metric: metric.to_f,
38
+ description: description,
39
+ )
40
+ end
41
+
42
+ def freebsd_fd
43
+ sys_used = Integer(`sysctl -n kern.openfiles`)
44
+ if sys_used > @limits[:fd][:critical]
45
+ alert 'fd sys', :critical, sys_used, "system is using #{sys_used} fds"
46
+ elsif sys_used > @limits[:fd][:warning]
47
+ alert 'fd sys', :warning, sys_used, "system is using #{sys_used} fds"
48
+ else
49
+ alert 'fd sys', :ok, sys_used, "system is using #{sys_used} fds"
50
+ end
51
+ end
52
+
53
+ def linux_fd
54
+ sys_used = Integer(`lsof | wc -l`)
55
+ if sys_used > @limits[:fd][:critical]
56
+ alert 'fd sys', :critical, sys_used, "system is using #{sys_used} fds"
57
+ elsif sys_used > @limits[:fd][:warning]
58
+ alert 'fd sys', :warning, sys_used, "system is using #{sys_used} fds"
59
+ else
60
+ alert 'fd sys', :ok, sys_used, "system is using #{sys_used} fds"
61
+ end
62
+
63
+ opts[:processes]&.each do |process|
64
+ used = Integer(`lsof -p #{process} | wc -l`)
65
+ name, _pid = `ps axo comm,pid | grep -w #{process}`.split
66
+ if used > @limits[:process][:critical]
67
+ alert "fd #{name} #{process}", :critical, used, "process #{name} #{process} is using #{used} fds"
68
+ elsif used > @limits[:process][:warning]
69
+ alert "fd #{name} #{process}", :warning, used, "process #{name} #{process} is using #{used} fds"
70
+ else
71
+ alert "fd #{name} #{process}", :ok, used, "process #{name} #{process} is using #{used} fds"
72
+ end
73
+ end
74
+ end
75
+
76
+ def tick
77
+ @fd.call
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'riemann/tools'
5
+
6
+ module Riemann
7
+ module Tools
8
+ class Freeswitch
9
+ include Riemann::Tools
10
+
11
+ opt :calls_warning, 'Calls warning threshold', default: 100
12
+ opt :calls_critical, 'Calls critical threshold', default: 300
13
+ opt :pid_file, 'FreeSWITCH daemon pidfile', type: String, default: '/var/run/freeswitch/freeswitch.pid'
14
+
15
+ def initialize
16
+ @limits = {
17
+ calls: { critical: opts[:calls_critical], warning: opts[:calls_warning] },
18
+ }
19
+ end
20
+
21
+ def dead_proc?(pid)
22
+ Process.getpgid(pid)
23
+ false
24
+ rescue Errno::ESRCH
25
+ true
26
+ end
27
+
28
+ def alert(service, state, metric, description)
29
+ report(
30
+ service: service.to_s,
31
+ state: state.to_s,
32
+ metric: metric.to_f,
33
+ description: description,
34
+ )
35
+ end
36
+
37
+ def exec_with_timeout(cmd, timeout)
38
+ pid = Process.spawn(cmd, { %i[err out] => :close, :pgroup => true })
39
+ begin
40
+ Timeout.timeout(timeout) do
41
+ Process.waitpid(pid, 0)
42
+ $CHILD_STATUS.exitstatus.zero?
43
+ end
44
+ rescue Timeout::Error
45
+ Process.kill(15, -Process.getpgid(pid))
46
+ puts "Killed pid: #{pid}"
47
+ false
48
+ end
49
+ end
50
+
51
+ def tick
52
+ # Determine how many current calls I have according to FreeSWITCH
53
+ fs_calls = `fs_cli -x "show calls count"| grep -Po '^\\d+'`.to_i
54
+
55
+ # Determine how many current channels I have according to FreeSWITCH
56
+ fs_channels = `fs_cli -x "show channels count"| grep -Po '^\\d+'`.to_i
57
+
58
+ # Determine how many conferences I have according to FreeSWITCH
59
+ fs_conferences = `fs_cli -x "conference list"| grep -Pco '^Conference'`.to_i
60
+
61
+ # Try to read pidfile. If it fails use Devil's dummy PID
62
+ begin
63
+ fs_pid = File.read(opts[:pid_file]).to_i
64
+ rescue StandardError
65
+ puts "Couldn't read pidfile: #{opts[:pid_file]}"
66
+ fs_pid = -666
67
+ end
68
+
69
+ fs_threads = fs_pid.positive? ? `ps huH p #{fs_pid} | wc -l`.to_i : 0
70
+
71
+ # Submit calls to riemann
72
+ if fs_calls > @limits[:calls][:critical]
73
+ alert 'FreeSWITCH current calls', :critical, fs_calls, "Number of calls are #{fs_calls}"
74
+ elsif fs_calls > @limits[:calls][:warning]
75
+ alert 'FreeSWITCH current calls', :warning, fs_calls, "Number of calls are #{fs_calls}"
76
+ else
77
+ alert 'FreeSWITCH current calls', :ok, fs_calls, "Number of calls are #{fs_calls}"
78
+ end
79
+
80
+ # Submit channels to riemann
81
+ if fs_channels > @limits[:calls][:critical]
82
+ alert 'FreeSWITCH current channels', :critical, fs_channels, "Number of channels are #{fs_channels}"
83
+ elsif fs_channels > @limits[:calls][:warning]
84
+ alert 'FreeSWITCH current channels', :warning, fs_channels, "Number of channels are #{fs_channels}"
85
+ else
86
+ alert 'FreeSWITCH current channels', :ok, fs_channels, "Number of channels are #{fs_channels}"
87
+ end
88
+
89
+ # Submit conferences to riemann
90
+ if fs_conferences > @limits[:calls][:critical]
91
+ alert 'FreeSWITCH current conferences', :critical, fs_conferences,
92
+ "Number of conferences are #{fs_conferences}"
93
+ elsif fs_conferences > @limits[:calls][:warning]
94
+ alert 'FreeSWITCH current conferences', :warning, fs_conferences,
95
+ "Number of conferences are #{fs_conferences}"
96
+ else
97
+ alert 'FreeSWITCH current conferences', :ok, fs_conferences, "Number of conferences are #{fs_conferences}"
98
+ end
99
+
100
+ # Submit threads to riemann
101
+ alert 'FreeSWITCH current threads', :ok, fs_threads, "Number of threads are #{fs_threads}" if fs_threads
102
+
103
+ # Submit status to riemann
104
+ if dead_proc?(fs_pid)
105
+ alert 'FreeSWITCH status', :critical, -1, 'FreeSWITCH service status: not running'
106
+ else
107
+ alert 'FreeSWITCH status', :ok, nil, 'FreeSWITCH service status: running'
108
+ end
109
+
110
+ # Submit CLI status to riemann using timeout in case it's unresponsive
111
+ if exec_with_timeout('fs_cli -x status', 2)
112
+ alert 'FreeSWITCH CLI status', :ok, nil, 'FreeSWITCH CLI status: responsive'
113
+ else
114
+ alert 'FreeSWITCH CLI status', :critical, -1, 'FreeSWITCH CLI status: not responding'
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'riemann/tools'
4
+
5
+ # Gathers haproxy CSV statistics and submits them to Riemann.
6
+ module Riemann
7
+ module Tools
8
+ class Haproxy
9
+ include Riemann::Tools
10
+ require 'net/http'
11
+ require 'csv'
12
+
13
+ opt :stats_url, 'Full url to haproxy stats (eg: https://user:password@host.com:9999/stats)', required: true,
14
+ type: :string
15
+
16
+ def initialize
17
+ @uri = URI("#{opts[:stats_url]};csv")
18
+ end
19
+
20
+ def tick
21
+ csv.each do |row|
22
+ row = row.to_hash
23
+ ns = "haproxy #{row['pxname']} #{row['svname']}"
24
+ row.each do |property, metric|
25
+ next if property.nil? || property == 'pxname' || property == 'svname'
26
+
27
+ report(
28
+ host: @uri.host,
29
+ service: "#{ns} #{property}",
30
+ metric: metric.to_f,
31
+ tags: ['haproxy'],
32
+ )
33
+ end
34
+
35
+ report(
36
+ host: @uri.host,
37
+ service: "#{ns} state",
38
+ state: (%w[UP OPEN].include?(row['status']) ? 'ok' : 'critical'),
39
+ tags: ['haproxy'],
40
+ )
41
+ end
42
+ end
43
+
44
+ def csv
45
+ http = ::Net::HTTP.new(@uri.host, @uri.port)
46
+ http.use_ssl = true if @uri.scheme == 'https'
47
+ http.start do |h|
48
+ get = ::Net::HTTP::Get.new(@uri.request_uri)
49
+ unless @uri.userinfo.nil?
50
+ userinfo = @uri.userinfo.split(':')
51
+ get.basic_auth userinfo[0], userinfo[1]
52
+ end
53
+ h.request get
54
+ end
55
+ CSV.parse(http.body.split('# ')[1], { headers: true })
56
+ end
57
+ end
58
+ end
59
+ end