check_passenger 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ *.sublime-workspace
24
+ *.sublime-project
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in check_passenger.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Alvaro Redondo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # CheckPassenger
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'check_passenger'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install check_passenger
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/[my-github-username]/check_passenger/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.test_files = FileList['test/test*.rb']
7
+ t.verbose = true
8
+ end
data/TODO.md ADDED
@@ -0,0 +1,12 @@
1
+ # Release
2
+
3
+ - README page.
4
+
5
+
6
+ # Other
7
+
8
+ - Option `all` to monitor all counters for all applications and globally,
9
+ without possibility to raise alerts.
10
+ - PerfDatum class.
11
+ - Option to set TTL used to estimate live processes.
12
+ - Option to set TTL of cached data.
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../lib', File.dirname(__FILE__))
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require 'thor'
7
+ require 'check_passenger'
8
+ require 'tmpdir'
9
+
10
+ class CheckPassengerCLI < Thor
11
+ include CheckPassenger::NagiosCheck
12
+
13
+ class_option :app_name, aliases: 'n', banner: 'Limit check to application with APP_NAME'
14
+ class_option :cache, aliases: 'C', type: :boolean,
15
+ banner: 'Cache data to avoid fast successive calls to passenger-status'
16
+ class_option :debug, aliases: 'D', type: :boolean, banner: 'Debug mode'
17
+ class_option :dump, aliases: 'd', type: :boolean, banner: 'Dump passenger-status output on error'
18
+ class_option :include_all, aliases: 'a', type: :boolean,
19
+ banner: 'Also include counter for all running apps'
20
+ class_option :passenger_status_path, aliases: 'p', banner: 'Path to passenger-status command'
21
+
22
+ desc 'memory', 'Check Passenger memory'
23
+ option :warn, banner: 'Memory usage threshold to raise warning status', aliases: 'w'
24
+ option :crit, banner: 'Memory usage threshold to raise critical status', aliases: 'c'
25
+ def memory
26
+ run_check do
27
+ CheckPassenger::Check.memory(options)
28
+ end
29
+ end
30
+
31
+ desc 'processes', 'Check Passenger processes'
32
+ option :warn, aliases: 'w', banner: 'Process count threshold to raise warning status'
33
+ option :crit, aliases: 'c', banner: 'Process count threshold to raise critical status'
34
+ def processes
35
+ run_check do
36
+ CheckPassenger::Check.process_count(options)
37
+ end
38
+ end
39
+
40
+ desc 'live_processes', 'Check Passenger live processes'
41
+ option :warn, aliases: 'w', banner: 'Live process count threshold to raise warning status'
42
+ option :crit, aliases: 'c', banner: 'Live process count threshold to raise critical status'
43
+ def live_processes
44
+ run_check do
45
+ CheckPassenger::Check.live_process_count(options)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def dump_passenger_status_output(exception = nil)
52
+ passenger_status_output = nil
53
+
54
+ if exception and exception.respond_to?(:passenger_status_output)
55
+ passenger_status_output = exception.passenger_status_output
56
+ end
57
+
58
+ if passenger_status_output.nil? and CheckPassenger::Check.parsed_data
59
+ passenger_status_output = CheckPassenger::Check.parsed_data.passenger_status_output
60
+ end
61
+
62
+ if passenger_status_output
63
+ filename = 'passenger_status_output_dump-%s.txt' % Time.now.strftime('%Y%m%d%H%M%S')
64
+ dump_path = File.expand_path(filename, Dir.tmpdir)
65
+ File.open(dump_path, 'wb') { |file| file.write passenger_status_output }
66
+ return dump_path
67
+ else
68
+ return nil
69
+ end
70
+ end
71
+
72
+ def run_check
73
+ if options[:include_all] and options[:app_name]
74
+ raise ArgumentError, 'Data for all apps can only be included when monitoring a global counter'
75
+ end
76
+
77
+ output_status, output_data = yield
78
+ nagios_output(output_status, output_data)
79
+
80
+ rescue StandardError => e
81
+ if options[:debug] or options[:dump]
82
+ if dump_path = dump_passenger_status_output(e)
83
+ message = e.message + ' -- passenger-status output dumped to %s' % dump_path
84
+ e = e.class.new(message)
85
+ end
86
+ end
87
+
88
+ if options[:debug]
89
+ raise e
90
+ else
91
+ case e
92
+ when CheckPassenger::StatusOutputError
93
+ nagios_error('Passenger UNKNOWN - An error occurred while parsing passenger-status output: %s' % e.message)
94
+ else
95
+ nagios_error('Passenger UNKNOWN - %s (%s)' % [e.message, e.class.to_s])
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ CheckPassengerCLI.start(ARGV)
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'check_passenger/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'check_passenger'
10
+ spec.version = CheckPassenger::VERSION
11
+ spec.authors = ['Alvaro Redondo']
12
+ spec.email = ['alvaro@redondo.name']
13
+ spec.summary = %q{Nagios check to monitor Passenger processes and memory}
14
+ # spec.description = %q{TODO: Write a longer description. Optional.}
15
+ spec.homepage = ''
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.6'
24
+ spec.add_development_dependency 'rake'
25
+ spec.add_development_dependency 'minitest'
26
+ spec.add_development_dependency 'minitest-reporters'
27
+
28
+ spec.add_dependency 'thor', '~> 0.19.1'
29
+ end
@@ -0,0 +1,13 @@
1
+ %w{
2
+ nagios_check
3
+ check
4
+ parser
5
+ passenger_status
6
+ status_output_error
7
+ version
8
+ }.each { |lib| require 'check_passenger/' + lib }
9
+
10
+ module CheckPassenger
11
+ LIVE_PROCESS_TTL_IN_SECONDS = 300
12
+ CACHE_TTL = 5
13
+ end
@@ -0,0 +1,96 @@
1
+ require 'tmpdir'
2
+
3
+ module CheckPassenger
4
+ class Check
5
+ class << self
6
+ include CheckPassenger::NagiosCheck
7
+
8
+ attr_reader :parsed_data
9
+
10
+ COUNTER_LABELS = {
11
+ live_process_count: '%d live processes',
12
+ memory: '%dMB memory used',
13
+ process_count: '%d processes'
14
+ }
15
+
16
+ def check_counter(counter_name, options = {})
17
+ load_parsed_data(options)
18
+ output_data = []
19
+
20
+ counter = parsed_data.send(counter_name.to_sym, options[:app_name])
21
+ output_status = nagios_status(counter, options)
22
+
23
+ data = {
24
+ text: 'Passenger %s %s - %s' %
25
+ [
26
+ options[:app_name] || parsed_data.passenger_version,
27
+ output_status.to_s.upcase,
28
+ COUNTER_LABELS[counter_name.to_sym] % counter
29
+ ],
30
+ counter: counter_name.to_s, value: counter,
31
+ warn: options[:warn], crit: options[:crit],
32
+ min: 0, max: nil
33
+ }
34
+ if !options[:app_name] and [:process_count, :live_process_count].include?(counter_name.to_sym)
35
+ data[:max] = parsed_data.max_pool_size
36
+ end
37
+ output_data << data
38
+
39
+ if !options[:app_name] and options[:include_all]
40
+ parsed_data.application_names.each do |app_name|
41
+ counter = parsed_data.send(counter_name.to_sym, app_name)
42
+ output_data << {
43
+ text: '%s %s' % [app_name, COUNTER_LABELS[counter_name.to_sym] % counter],
44
+ counter: counter_name.to_s, value: counter
45
+ }
46
+ end
47
+ end
48
+
49
+ return [output_status, output_data]
50
+ end
51
+
52
+ def method_missing(method, *args)
53
+ if COUNTER_LABELS.keys.include?(method)
54
+ check_counter(method, *args)
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ def respond_to?(method)
61
+ return true if COUNTER_LABELS.keys.include?(method)
62
+ super
63
+ end
64
+
65
+ private
66
+
67
+ def load_parsed_data(options)
68
+ @parsed_data = options[:parsed_data]
69
+
70
+ if @parsed_data.nil? and options[:cache]
71
+ cache_file_path = File.expand_path('check_passenger_cache.dump', Dir.tmpdir)
72
+
73
+ if File.exist?(cache_file_path) and (Time.now - File.mtime(cache_file_path) < CACHE_TTL)
74
+ File.open(cache_file_path, 'rb') { |file| @parsed_data = Marshal.load(file.read) }
75
+ end
76
+ end
77
+
78
+ if @parsed_data.nil?
79
+ @parsed_data = Parser.new(passenger_status(options[:passenger_status_path]).run)
80
+
81
+ if options[:cache]
82
+ File.open(cache_file_path, 'wb') { |file| file.write Marshal.dump(@parsed_data) }
83
+ end
84
+ end
85
+
86
+ @parsed_data
87
+ end
88
+
89
+ def passenger_status(passenger_status_path = nil)
90
+ @passenger_status ||= PassengerStatus
91
+ @passenger_status.path = passenger_status_path
92
+ @passenger_status
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,66 @@
1
+ module CheckPassenger
2
+ module NagiosCheck
3
+ EXIT_CODES = { ok: 0, warn: 1, crit: 2 }
4
+
5
+ private
6
+
7
+ def nagios_error(message)
8
+ puts message
9
+ exit 3
10
+ end
11
+
12
+ def nagios_output(status, data)
13
+ unless [:ok, :warn, :crit].include?(status)
14
+ raise ArgumentError, 'Invalid status provided: %s' % status.to_s
15
+ end
16
+
17
+ data = [data] unless data.is_a?(Array)
18
+
19
+ data.each do |line|
20
+ raise ArgumentError, 'No status text provided' unless line.has_key?(:text)
21
+
22
+ if line[:counter] and line[:value]
23
+ puts '%s|%s=%d;%s;%s;%s;%s' % [
24
+ line[:text],
25
+ line[:counter], line[:value],
26
+ line[:warn], line[:crit],
27
+ line[:min], line[:max]
28
+ ]
29
+ else
30
+ puts line[:text]
31
+ end
32
+ end
33
+
34
+ exit EXIT_CODES[status]
35
+ end
36
+
37
+ def nagios_range_to_condition(nagios_range)
38
+ case nagios_range
39
+ when /^(-?\d+)$/
40
+ lambda { |n| !(0 .. $1.to_i).include?(n) }
41
+ when /^(-?\d+):~?$/
42
+ lambda { |n| n < $1.to_i }
43
+ when /^~?:(-?\d+)$/
44
+ lambda { |n| n > $1.to_i }
45
+ when /^(-?\d+):(-?\d+)$/
46
+ lambda { |n| !($1.to_i .. $2.to_i).include?(n) }
47
+ when /^@(-?\d+):(-?\d+)$/
48
+ lambda { |n| ($1.to_i .. $2.to_i).include?(n) }
49
+ else
50
+ raise ArgumentError, 'Cannot process Nagios range: %s' % nagios_range
51
+ end
52
+ end
53
+
54
+ def nagios_status(counter, options = {})
55
+ status = :ok
56
+ [:warn, :crit].each do |level|
57
+ status = level if options[level] && number_outside_range?(counter, options[level])
58
+ end
59
+ status
60
+ end
61
+
62
+ def number_outside_range?(number, nagios_range)
63
+ nagios_range_to_condition(nagios_range).call(number)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,120 @@
1
+ module CheckPassenger
2
+ class Parser
3
+ UNIT_MULTIPLIERS = { 's' => 1, 'm' => 60, 'h' => 3_600, 'd' => 86_400 }
4
+
5
+ attr_reader :max_pool_size, :passenger_status_output, :passenger_version
6
+
7
+ def initialize(passenger_status_output)
8
+ @passenger_status_output = passenger_status_output
9
+ parse_passenger_status_output
10
+ end
11
+
12
+ def application_names
13
+ @application_data.map { |app_data| app_data[:name] }
14
+ end
15
+
16
+ def live_process_count(app_name = nil)
17
+ if app_name
18
+ app_data = application_data(app_name)
19
+ app_data[:live_process_count]
20
+ else
21
+ @application_data.inject(0) { |sum, e| sum + e[:live_process_count] }
22
+ end
23
+ end
24
+
25
+ def memory(app_name = nil)
26
+ if app_name
27
+ app_data = application_data(app_name)
28
+ app_data[:memory]
29
+ else
30
+ @application_data.inject(0) { |sum, e| sum + e[:memory] }
31
+ end
32
+ end
33
+
34
+ def process_count(app_name = nil)
35
+ if app_name
36
+ app_data = application_data(app_name)
37
+ app_data[:process_count]
38
+ else
39
+ @process_count
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def application_data(app_name)
46
+ if app_name
47
+ data = @application_data.select { |d| d[:name].include?(app_name) }
48
+ if data.size == 0
49
+ raise ArgumentError, 'Could not find an application by "%s"' % app_name
50
+ elsif data.size > 1
51
+ raise ArgumentError, 'More than one application match "%s"' % app_name
52
+ else
53
+ return data.first
54
+ end
55
+ else
56
+ return @application_data
57
+ end
58
+ end
59
+
60
+ def is_process_alive?(last_used)
61
+ life_to_seconds(last_used) < LIVE_PROCESS_TTL_IN_SECONDS
62
+ end
63
+
64
+ def life_to_seconds(last_used)
65
+ last_used.split(/\s+/).inject(0) do |sum, part|
66
+ if part =~ /^(\d+)([a-z])$/
67
+ unless UNIT_MULTIPLIERS.has_key?($2)
68
+ raise StatusOutputError.new(
69
+ 'Unknown time unit "%s" in "%s"' % [$2, last_used],
70
+ passenger_status_output
71
+ )
72
+ end
73
+ sum + $1.to_i * UNIT_MULTIPLIERS[$2]
74
+ else
75
+ sum
76
+ end
77
+ end
78
+ end
79
+
80
+ def parse_application_data(output)
81
+ output.split("\n\n").map do |app_output|
82
+ app_data = {}
83
+
84
+ app_output =~ /App root: +([^\n]+)/
85
+ raise StatusOutputError.new('Could not find app name', passenger_status_output) unless $1
86
+ app_data[:name] = $1.strip
87
+
88
+ app_data[:process_count] = app_output.scan(/PID *: *\d+/).size
89
+ app_data[:memory] = app_output.scan(/Memory *: *(\d+)M/).inject(0.0) { |s, m| s + m[0].to_f }
90
+ app_data[:live_process_count] = (
91
+ app_output.scan(/Last used *: *([^\n]+)/).select { |m| is_process_alive?(m[0]) }
92
+ ).size
93
+
94
+ app_data
95
+ end
96
+ end
97
+
98
+ def parse_passenger_status_output
99
+ passenger_status_output =~ /^(.*?)-+ +Application groups +-+[^\n]*\n(.*)$/m
100
+ raise StatusOutputError.new('Did not find "Application groups" section', passenger_status_output) unless $1
101
+
102
+ generic_data = $1
103
+ application_data = $2
104
+
105
+ generic_data =~ /Version *: *([.\d]+)/
106
+ raise StatusOutputError.new('Could not find Passenger version', passenger_status_output) unless $1
107
+ @passenger_version = $1
108
+
109
+ generic_data =~ /Max pool size *: *(\d+)/
110
+ raise StatusOutputError.new('Could not find max pool size', passenger_status_output) unless $1
111
+ @max_pool_size = $1.to_i
112
+
113
+ generic_data =~ /Processes *: *(\d+)/
114
+ raise StatusOutputError.new('Could not find process count', passenger_status_output) unless $1
115
+ @process_count = $1.to_i
116
+
117
+ @application_data = parse_application_data(application_data)
118
+ end
119
+ end
120
+ end