Class: Pinglish

Inherits:
Object
  • Object
show all
Defined in:
lib/pinglish.rb,
lib/pinglish/check.rb

Overview

This Rack app provides an endpoint for configurable system health checks. It’s intended to be consumed by machines.

Defined Under Namespace

Classes: Check, TooLong

Constant Summary collapse

HEADERS =

The HTTP headers sent for every response.

{
  "Content-Type" => "application/json; charset=UTF-8"
}

Instance Method Summary collapse

Constructor Details

#initialize(options = nil) {|_self| ... } ⇒ Pinglish

Create a new instance of the app, with optional parameter ‘:max` timeout in seconds (default: `29`); yields itself to an optional block for configuring checks.

Yields:

  • (_self)

Yield Parameters:

  • _self (Pinglish)

    the object that the method was called on



20
21
22
23
24
25
26
27
# File 'lib/pinglish.rb', line 20

def initialize(options=nil, &block)
  options ||= {}

  @checks = {}
  @max    = options[:max] || 29 # seconds

  yield self if block_given?
end

Instance Method Details

#call(env) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/pinglish.rb', line 29

def call(env)
  request = Rack::Request.new(env)

  begin
    timeout @max do
      results = {}

      selected_checks(request.params).each do |check|
        begin
          timeout(check.timeout) do
            results[check.name] = check.call
          end
        rescue => e
          results[check.name] = e
        end
      end

      failed = results.values.any? { |v| failure? v }
      http_status = failed ? 503 : 200
      text_status = failed ? "failures" : "ok"

      data = {
        :now    => Time.now.to_i.to_s,
        :status => text_status
      }

      results.each do |name, value|
        if failure?(value)
          # If a check fails its name is added to a `failures` array.
          # If the check failed because it timed out, its name is
          # added to a `timeouts` array instead.

          key = timeout?(value) ? :timeouts : :failures
          (data[key] ||= []) << name

          if key == :failures and value.is_a?(Exception)
            data[name] = {
              state: :error,
              exception: value.class.name,
              message: value.message,
            }
          end

        elsif value
          # If the check passed and returned a value, the stringified
          # version of the value is returned under the `name` key.

          data[name] = value
        end
      end

      [http_status, HEADERS, [JSON.generate(data)]]
    end

  rescue Exception => ex
    # Something catastrophic happened. We can't even run the checks
    # and render a JSON response. Fall back on a pre-rendered string
    # and interpolate the current epoch time.

    now = Time.now.to_i.to_s

    body = "    {\n      \"status\": \"failures\",\n      \"now\": \"\#{now}\",\n      \"error\": {\n        \"class\": \"\#{ex.class.name}\",\n        \"message\": \"\#{ex.message.tr('\"', '')}\"\n      }\n    }\n    EOF\n\n    [500, HEADERS, [body]]\n  end\nend\n".gsub(/^ {6}/, '')

#check(name = :default, options = nil, &block) ⇒ Object

Add a new check with optional ‘name`. A `:timeout` option can be specified in seconds for checks that might take longer than the one second default. A previously added check with the same name will be replaced.



118
119
120
# File 'lib/pinglish.rb', line 118

def check(name = :default, options = nil, &block)
  @checks[name.to_sym] = Check.new(name, options, &block)
end

#failure?(value) ⇒ Boolean

Does ‘value` represent a check failure? This default implementation returns `true` for any value that is an Exception or false. Subclasses can override this method for different behavior.

Returns:

  • (Boolean)


126
127
128
# File 'lib/pinglish.rb', line 126

def failure?(value)
  value.is_a?(Exception) || value == false
end

#selected_checks(params) ⇒ Object



105
106
107
108
109
110
111
# File 'lib/pinglish.rb', line 105

def selected_checks(params)
  if (selected = params['checks'])
    selected = selected.split(',').map(&:to_sym)
    return @checks.values_at(*selected).compact
  end
  @checks.values
end

#timeout(seconds, &block) ⇒ Object

Raise Pinglish::TooLong after ‘seconds` has elapsed. This default implementation uses Ruby’s built-in Timeout class. Subclasses can override this method for different behavior, but any new implementation must raise Pinglish::TooLong when the timeout is exceeded or override ‘timeout?` appropriately.



136
137
138
# File 'lib/pinglish.rb', line 136

def timeout(seconds, &block)
  Timeout.timeout seconds, Pinglish::TooLong, &block
end

#timeout?(value) ⇒ Boolean

Does ‘value` represent a check timeout? Returns `true` for any value that is an instance of Pinglish::TooLong.

Returns:

  • (Boolean)


143
144
145
# File 'lib/pinglish.rb', line 143

def timeout?(value)
  value.is_a? Pinglish::TooLong
end