app_status 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -63,6 +63,8 @@ This is where you set up the checks which you want to be run when
63
63
  someone hits the URL above. Set up some calls which evaluate the health
64
64
  of your application and call `add_check` for each one.
65
65
 
66
+ #### add_check
67
+
66
68
  `add_check` expects a service name, plus a block to be evaluated to determine
67
69
  the health of that service. The block should return either a status value, or
68
70
  a 2-element array with status and some details.
@@ -94,6 +96,37 @@ Valid status values (in ascending order of seriousness) are:
94
96
 
95
97
  These are set up to be compatible with Nagios.
96
98
 
99
+ #### add_description
100
+
101
+ `add_description` allows you to specify extended description and troubleshooting
102
+ information for any check which has been added via `add_check`.
103
+
104
+ Currently these are not returned via the JSON endpoint, but are available
105
+ via the HTML status page.
106
+
107
+ Description information is parsed by kramdown for display. Refer to the
108
+ [kramdown style guide](http://kramdown.rubyforge.org/quickref.html) for
109
+ usage information.
110
+
111
+ ```ruby
112
+ AppStatus::CheckCollection.configure do |c|
113
+
114
+ c.add_check('some_service') do
115
+ [:critical, 'what is going on']
116
+ end
117
+ c.add_description 'some_service', <<-EOF
118
+ some_service failures indicate that some_service is going wrong.
119
+
120
+ this is handy since nagios really requires brief output, but sometimes you need
121
+ more space to explain what a check is.
122
+
123
+ think of it as the answer to the problem of "That guy is on vaction, but his
124
+ app is raising alarms. WTF do I do?"
125
+ EOF
126
+
127
+ end
128
+ ```
129
+
97
130
  Keep in mind that anyone who hits your status URL can cause your checks to run,
98
131
  so if they expose sensitive data or are a potential DOS vector you should
99
132
  probably protect them with some kind of authentication.
@@ -4,6 +4,9 @@ module AppStatus
4
4
 
5
5
  class CheckCollection
6
6
 
7
+ include Enumerable
8
+
9
+ @@check_descriptions = HashWithIndifferentAccess.new
7
10
  @@checks = HashWithIndifferentAccess.new
8
11
 
9
12
  # Add checks here.
@@ -28,89 +31,117 @@ module AppStatus
28
31
  end
29
32
 
30
33
  def self.clear_checks!
34
+ @@check_descriptions = HashWithIndifferentAccess.new
31
35
  @@checks = HashWithIndifferentAccess.new
32
36
  end
33
37
 
34
- def initialize
35
- @valid_status = {
38
+ def self.valid_status_map
39
+ {
36
40
  ok: 0,
37
41
  warning: 1,
38
42
  critical: 2,
39
43
  unknown: 3
40
- }.freeze
41
-
42
- @check_results = HashWithIndifferentAccess.new
43
- @eval_finished = nil
44
- @eval_time = 0
44
+ }
45
45
  end
46
46
 
47
47
  # add the results of a check to the collection.
48
48
  # this should describe the health of some portion of your application
49
+ # The check block you supply should return a status value, or a
50
+ # [:status, 'details'] array.
49
51
  #
50
52
  # example:
51
- # value = some_service_check
52
- # c.add(:name => 'some_service', :status => :ok, :details => value)
53
+ # AppStatus::CheckCollection.add_check('some_service') do
54
+ # [:ok, 'these are optional details']
55
+ # end
53
56
  def self.add_check(name, &block)
54
57
  raise ArgumentError, ":name option is required." if ! name
55
- # raise ArgumentError, ":status option is required." if ! options[:status]
56
58
 
57
59
  name = name.to_sym
58
60
  raise ArgumentError, "Check name '#{name}' has already been added." if @@checks.keys.include?(name.to_s)
59
61
  raise ArgumentError, "No check defined for '#{name}'." if ! block_given?
60
62
 
61
- @@checks[name] = block
63
+ item = CheckItem.new(name)
64
+ item.proc = block
65
+ @@checks[name] = item
62
66
  end
63
67
 
64
- def valid_status?(status)
65
- @valid_status.keys.include?(status)
68
+ # add a long-form description of a check
69
+ # service must have already been added via add_check.
70
+ #
71
+ # example:
72
+ # AppStatus::CheckCollection.configure do |c|
73
+ # c.add_check('some_service') do
74
+ # [:ok, 'these are optional details']
75
+ # end
76
+ # c.add_decription 'some_service', <<-EOF
77
+ # some_service is pretty easy to understand.
78
+ # it always works, no matter what.
79
+ # but if it were **harder to comprehend** you
80
+ # could add markdown here to explain what it is
81
+ # and what to do if it starts failing.
82
+ # EOF
83
+ #
84
+ def self.add_description(name, markdown)
85
+ raise ArgumentError, "Check '#{name}' is not defined." if ! @@checks[name]
86
+ @@checks[name].description = markdown
87
+ end
88
+
89
+
90
+
91
+ attr_reader :finished, :ms, :status, :status_code
92
+
93
+
94
+ def initialize
95
+ reset
96
+ end
97
+
98
+ def reset
99
+ @finished = nil
100
+ @ms = 0
101
+ @status = :ok
102
+ @status_code = self.class.valid_status_map[@status]
103
+ @@checks.each {|key,check| check.reset }
104
+ end
105
+
106
+ def each
107
+ @@checks.each {|name,check| yield check }
66
108
  end
67
109
 
68
110
  # run the checks added via configure
69
111
  # results of the checks are available via as_json
70
112
  def evaluate!
71
113
  eval_start = Time.now
72
- @check_results = {}
73
- @@checks.each do |name,proc|
74
- check_start = Time.now
75
- status, details = proc.call
76
- check_time = (Time.now - check_start) * 1000
77
-
78
- status = status.to_sym if status
79
- details = details.to_s if details
80
-
81
- if ! valid_status?(status)
82
- details = "Check returned invalid status '#{status}'. #{details}".strip
83
- status = :unknown
84
- end
85
- @check_results[name] = {
86
- status: status,
87
- status_code: @valid_status[status],
88
- details: details,
89
- ms: check_time.to_i
90
- }
114
+
115
+ reset
116
+
117
+ @@checks.each do |name,check|
118
+ @status_code = [check.evaluate!, @status_code].max
91
119
  end
92
120
 
93
- @eval_finished = Time.now.utc
94
- @eval_time = (Time.now - eval_start) * 1000
95
- end
121
+ @finished = Time.now.utc
122
+ @ms = (Time.now - eval_start) * 1000
96
123
 
97
- def as_json
98
- if @check_results.size == 0
99
- max_status = :unknown
100
- max_int = @valid_status[max_status]
124
+ if @@checks.size == 0
125
+ @status = :unknown
126
+ @status_code = self.class.valid_status_map[@status]
101
127
  else
102
- max_int = @check_results.inject([]){ |memo,val| memo << val[1][:status_code]; memo}.max
103
- max_status = @valid_status.invert[max_int]
128
+ @status = self.class.valid_status_map.invert[@status_code]
104
129
  end
130
+ end
105
131
 
132
+ def as_hash
106
133
  HashWithIndifferentAccess.new({
107
- status: max_status,
108
- status_code: max_int,
109
- ms: @eval_time.to_i,
110
- finished: @eval_finished.iso8601,
111
- checks: @check_results
134
+ status: @status,
135
+ status_code: @status_code,
136
+ ms: @ms.to_i,
137
+ finished: @finished.iso8601,
138
+ checks: @@checks.inject({}) {|memo,(name,check)| memo[name] = check.as_hash; memo}
112
139
  })
113
140
  end
141
+
142
+ def as_json
143
+ as_hash
144
+ end
114
145
  end
115
146
 
116
147
  end
@@ -0,0 +1,58 @@
1
+ require 'kramdown'
2
+
3
+ module AppStatus
4
+ class CheckItem
5
+ attr_reader :name, :status, :status_code, :details, :ms
6
+ attr_accessor :proc, :description
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ @proc = nil
11
+ @description = ''
12
+
13
+ reset
14
+ end
15
+
16
+ def reset
17
+ @result = {}
18
+ end
19
+
20
+ def valid_status?(status)
21
+ CheckCollection.valid_status_map.keys.include?(status)
22
+ end
23
+
24
+ def evaluate!
25
+ check_start = Time.now
26
+ status, details = @proc ? @proc.call : [:unknown, "Check is not configured."]
27
+ check_time = (Time.now - check_start) * 1000
28
+
29
+ status = status.to_sym if status
30
+ details = details.to_s if details
31
+
32
+ if ! valid_status?(status)
33
+ details = "Check returned invalid status '#{status}'. #{details}".strip
34
+ status = :unknown
35
+ end
36
+
37
+ @status = status
38
+ @status_code = CheckCollection.valid_status_map[@status]
39
+ @details = details
40
+ @ms = check_time.to_i
41
+
42
+ return @status_code
43
+ end
44
+
45
+ def as_hash
46
+ {
47
+ status: @status,
48
+ status_code: @status_code,
49
+ details: @details,
50
+ ms: @ms
51
+ }
52
+ end
53
+
54
+ def html_description
55
+ Kramdown::Document.new(@description).to_html
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ <div class="app_status">
2
+
3
+ <h1 class="<%= @checks.status %>">Current Status : <%= @checks.status %></h1>
4
+
5
+ <table>
6
+ <% @checks.each do |check| %>
7
+ <tr class="check <%= check.status %> <%= check.name %>" data-check-name="<%= check.name %>">
8
+ <td class="status"><%= check.status %></td>
9
+ <td class="name"><%= check.name %></td>
10
+ <td class="details"><%= check.details %></td>
11
+ <td class="ms"><%= check.ms %>ms</td>
12
+ </tr>
13
+ <% if check.description.present? -%>
14
+ <tr class="<%= check.name %> description" style="display: none;">
15
+ <td colspan="4">
16
+ <%= check.html_description.html_safe %>
17
+ </td>
18
+ </tr>
19
+ <% end %>
20
+ <% end %>
21
+ </table>
22
+
23
+ </div>
24
+
25
+ <script>
26
+ $(function() {
27
+ $('.app_status .check').click(function() {
28
+ var checkName = $(this).data('check-name');
29
+ $('.'+checkName+'.description').fadeToggle();
30
+ })
31
+ })
32
+ </script>
data/bin/kramdown ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file was generated by Bundler.
4
+ #
5
+ # The application 'kramdown' is installed as part of a gem, and
6
+ # this file is here to facilitate running it.
7
+ #
8
+
9
+ require 'pathname'
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
11
+ Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('kramdown', 'kramdown')
@@ -1,3 +1,3 @@
1
1
  module AppStatus
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/app_status.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "app_status/engine"
2
- require "haml"
3
2
 
4
3
  module AppStatus
5
4
  end
@@ -0,0 +1,28 @@
1
+ AppStatus::CheckCollection.configure do |c|
2
+ c.add_check('ok') do
3
+ [:ok, "looks good"]
4
+ end
5
+ c.add_description 'ok', <<-EOF
6
+ ### OK Check
7
+
8
+ This check is always ok.
9
+ EOF
10
+
11
+ c.add_check('crit') do
12
+ [:critical, "fail"]
13
+ end
14
+ c.add_description 'crit', <<-EOF
15
+ ### Critical Check
16
+
17
+ This check is always critical.
18
+ EOF
19
+
20
+ c.add_check('no_description') do
21
+ [:ok, 'check has no longer description']
22
+ end
23
+
24
+ c.add_check('no_details') do
25
+ :ok
26
+ end
27
+ c.add_description('no_details', 'check only returns a status.')
28
+ end
@@ -1,4 +1,3 @@
1
1
  Rails.application.routes.draw do
2
-
3
2
  mount AppStatus::Engine => "/status"
4
3
  end