check_passenger 0.1

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 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