chequeo 0.2.0.beta
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +38 -0
- data/Rakefile +2 -0
- data/bin/chequeo +19 -0
- data/chequeo.gemspec +37 -0
- data/init.rb +0 -0
- data/lib/chequeo.rb +38 -0
- data/lib/chequeo/checkup_processor.rb +24 -0
- data/lib/chequeo/cli.rb +188 -0
- data/lib/chequeo/configuration.rb +39 -0
- data/lib/chequeo/healthchecks/base.rb +56 -0
- data/lib/chequeo/healthchecks/model_validity.rb +9 -0
- data/lib/chequeo/healthchecks/system_up.rb +18 -0
- data/lib/chequeo/healthchecks/test_check.rb +15 -0
- data/lib/chequeo/manager.rb +27 -0
- data/lib/chequeo/notifications/base.rb +45 -0
- data/lib/chequeo/notifications/logger.rb +60 -0
- data/lib/chequeo/notifications/slack.rb +61 -0
- data/lib/chequeo/notifications/twilio.rb +11 -0
- data/lib/chequeo/notifications/webhook.rb +11 -0
- data/lib/chequeo/processor.rb +31 -0
- data/lib/chequeo/scheduled_job.rb +24 -0
- data/lib/chequeo/scheduler.rb +94 -0
- data/lib/chequeo/version.rb +5 -0
- data/lib/generators/chequeo/install_generator.rb +11 -0
- data/lib/generators/chequeo/templates/initializer.rb +3 -0
- metadata +226 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 170c778706448e9be5cb4c364a0e74a2daeb35cb
|
4
|
+
data.tar.gz: 469cd91e6a037e35287a4884d11f04c67f3f21ef
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a87218a7eae2d41ec696676115a930a932cc49d3938d114f93113e9f1f8af49c5a0f0c3e7dd0936c205c8fabab3d0e941b21115aabbc411973653b6c4e26139a
|
7
|
+
data.tar.gz: 27e7c79fdfd7b9025f2a832c890c685c55afbeb6cf9cb83975e0724c4ec69603395864ac4523466c96b33e9a5086fc22128b73d0ea634253f01cd41f580e2871
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.idea
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2018 Jonathan De Jong
|
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,38 @@
|
|
1
|
+
# chequeo
|
2
|
+
|
3
|
+
Chequeo provides a framework for running checkups on your application. The goal of checkups is to detect problems in your application and take actions to alert or repair the issue.
|
4
|
+
|
5
|
+
This could be sending an alert to Slack, a text message, hitting a webhook URL or logging out the alert to a database.
|
6
|
+
|
7
|
+
## Inspiration
|
8
|
+
|
9
|
+
After struggeling internally to deal with building health checks into our system and hearing a wonderful talk at RailsConf 2018 by Ryan Laughlin ([Video](http://confreaks.tv/videos/railsconf2018-the-doctor-is-in-using-checkups-to-find-bugs-in-production)) I decided it would be a good idea to build a library to make checkups easier.
|
10
|
+
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'galactic-senate'
|
18
|
+
```
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install galactic-senate
|
27
|
+
|
28
|
+
Generate a config file into your initializers directory.
|
29
|
+
|
30
|
+
$ rails generate chequeo:install
|
31
|
+
|
32
|
+
### Processor
|
33
|
+
In order to run the checkups we need a processor. Currently we have a standalone daemon that manages the jobs.
|
34
|
+
|
35
|
+
#### Standalone
|
36
|
+
|
37
|
+
|
38
|
+
|
data/Rakefile
ADDED
data/bin/chequeo
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Quiet some warnings we see when running in warning mode:
|
4
|
+
# RUBYOPT=-w bundle exec sidekiq
|
5
|
+
$TESTING = false
|
6
|
+
|
7
|
+
require_relative '../lib/chequeo/cli'
|
8
|
+
|
9
|
+
begin
|
10
|
+
cli = Chequeo::CLI.instance
|
11
|
+
|
12
|
+
cli.setup
|
13
|
+
cli.start
|
14
|
+
rescue => e
|
15
|
+
raise e if $DEBUG
|
16
|
+
STDERR.puts e.message
|
17
|
+
STDERR.puts e.backtrace.join("\n")
|
18
|
+
exit 1
|
19
|
+
end
|
data/chequeo.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "chequeo/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "chequeo"
|
8
|
+
spec.version = Chequeo::VERSION
|
9
|
+
spec.authors = ["jdejong"]
|
10
|
+
spec.email = [""]
|
11
|
+
|
12
|
+
spec.summary = 'Checkups Made Easy'
|
13
|
+
spec.description = 'A framework to make running checkups on your platform easier.'
|
14
|
+
spec.homepage = 'https://github.com/jdejong/chequeo'
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
|
21
|
+
spec.executables = ['chequeo']
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.required_ruby_version = '>= 2.2.0'
|
25
|
+
|
26
|
+
spec.add_runtime_dependency 'twilio-ruby', '>= 5.0', '< 6.0'
|
27
|
+
spec.add_runtime_dependency 'slack-ruby-client', '>= 0.9.0'
|
28
|
+
spec.add_runtime_dependency 'fugit', '>= 1.1.5'
|
29
|
+
spec.add_runtime_dependency 'concurrent-ruby', ">= 1.0"
|
30
|
+
spec.add_runtime_dependency 'galactic-senate', '~> 0.1'
|
31
|
+
spec.add_runtime_dependency 'oj', '>= 3.6', '< 4'
|
32
|
+
spec.add_runtime_dependency 'redis', '>= 4.0'
|
33
|
+
|
34
|
+
spec.add_development_dependency 'rake', '~> 10'
|
35
|
+
spec.add_development_dependency 'minitest', '~> 5.3'
|
36
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
37
|
+
end
|
data/init.rb
ADDED
File without changes
|
data/lib/chequeo.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chequeo
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :config
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.config
|
10
|
+
@config ||= Chequeo::Configuration.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.reset
|
14
|
+
@config = Chequeo::Configuration.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.configure
|
18
|
+
yield(config)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'chequeo/configuration'
|
23
|
+
|
24
|
+
require 'chequeo/processor'
|
25
|
+
require 'chequeo/scheduler'
|
26
|
+
require 'chequeo/checkup_processor'
|
27
|
+
require 'chequeo/scheduled_job'
|
28
|
+
require 'chequeo/manager'
|
29
|
+
|
30
|
+
require 'chequeo/healthchecks/base'
|
31
|
+
require 'chequeo/healthchecks/test_check'
|
32
|
+
require 'chequeo/healthchecks/system_up'
|
33
|
+
|
34
|
+
require 'chequeo/notifications/base'
|
35
|
+
require 'chequeo/notifications/slack'
|
36
|
+
require 'chequeo/notifications/twilio'
|
37
|
+
require 'chequeo/notifications/webhook'
|
38
|
+
require 'chequeo/notifications/logger'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chequeo
|
4
|
+
class CheckupProcessor < Chequeo::Processor
|
5
|
+
|
6
|
+
def process_task
|
7
|
+
while !@completed
|
8
|
+
|
9
|
+
_job_to_process = Chequeo.config.redis.lpop("chequeo-job-queue")
|
10
|
+
process_one _job_to_process if _job_to_process
|
11
|
+
|
12
|
+
sleep 0.1 if !_job_to_process
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def process_one( job )
|
17
|
+
_job = Chequeo::HealthChecks::Base.deserialize(job)
|
18
|
+
|
19
|
+
_job.process
|
20
|
+
_job.notify
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
data/lib/chequeo/cli.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$stdout.sync = true
|
4
|
+
|
5
|
+
require 'singleton'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
module Chequeo
|
9
|
+
class CLI
|
10
|
+
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
DEFAULTS = {
|
14
|
+
|
15
|
+
}
|
16
|
+
|
17
|
+
SIGNAL_HANDLERS = {
|
18
|
+
# Ctrl-C in terminal
|
19
|
+
'INT' => ->(cli) { raise Interrupt },
|
20
|
+
# TERM is the signal that the daemon must exit.
|
21
|
+
'TERM' => ->(cli) { raise Interrupt },
|
22
|
+
'USR1' => ->(cli) {
|
23
|
+
Rails.logger.info "Received USR1, no longer accepting requests"
|
24
|
+
cli.launcher.quiet
|
25
|
+
},
|
26
|
+
'TSTP' => ->(cli) {
|
27
|
+
Rails.logger.info "Received TSTP, no longer accepting requests"
|
28
|
+
cli.launcher.quiet
|
29
|
+
},
|
30
|
+
'USR2' => ->(cli) {
|
31
|
+
|
32
|
+
},
|
33
|
+
'TTIN' => ->(cli) {
|
34
|
+
Thread.list.each do |thread|
|
35
|
+
Rails.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)}"
|
36
|
+
if thread.backtrace
|
37
|
+
Rails.logger.warn thread.backtrace.join("\n")
|
38
|
+
else
|
39
|
+
Rails.logger.warn "<no backtrace available>"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
},
|
43
|
+
}
|
44
|
+
|
45
|
+
def self.options
|
46
|
+
@options ||= DEFAULTS.dup
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.options=(opts)
|
50
|
+
@options = opts
|
51
|
+
end
|
52
|
+
|
53
|
+
def options
|
54
|
+
Chequeo::CLI.options
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup(args=ARGV)
|
58
|
+
setup_options(args)
|
59
|
+
require_system
|
60
|
+
|
61
|
+
write_pid
|
62
|
+
end
|
63
|
+
|
64
|
+
def start
|
65
|
+
Chequeo.config.logger.debug "Starting Daemon"
|
66
|
+
|
67
|
+
self_read, self_write = IO.pipe
|
68
|
+
sigs = %w(INT TERM TTIN TSTP USR1 USR2)
|
69
|
+
|
70
|
+
sigs.each do |sig|
|
71
|
+
begin
|
72
|
+
trap sig do
|
73
|
+
self_write.write("#{sig}\n")
|
74
|
+
end
|
75
|
+
rescue ArgumentError
|
76
|
+
puts "Signal #{sig} not supported"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
begin
|
81
|
+
|
82
|
+
#spwan scheduler and worker pools
|
83
|
+
|
84
|
+
Chequeo.config.logger.debug "Starting Schedule Process"
|
85
|
+
Chequeo::Scheduler.new(nil).process
|
86
|
+
Chequeo.config.logger.debug "Done Starting Schedule Process"
|
87
|
+
|
88
|
+
Chequeo::Manager.new.process
|
89
|
+
|
90
|
+
while readable_io = IO.select([self_read])
|
91
|
+
signal = readable_io.first[0].gets.strip
|
92
|
+
puts "*** SIGNAL RECEIVED :: #{signal} ***"
|
93
|
+
handle_signal(signal)
|
94
|
+
end
|
95
|
+
rescue Interrupt
|
96
|
+
Rails.logger.info 'Shutting down'
|
97
|
+
#launcher.stop
|
98
|
+
|
99
|
+
Rails.logger.info "Bye!"
|
100
|
+
exit(0)
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
def handle_signal(sig)
|
106
|
+
Rails.logger.debug "Got #{sig} signal"
|
107
|
+
handy = SIGNAL_HANDLERS[sig]
|
108
|
+
if handy
|
109
|
+
handy.call(self)
|
110
|
+
else
|
111
|
+
Rails.logger.info { "No signal handler for #{sig}" }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def setup_options(args)
|
118
|
+
opts = parse_options(args)
|
119
|
+
|
120
|
+
|
121
|
+
options.merge!(opts)
|
122
|
+
end
|
123
|
+
|
124
|
+
def parse_options(argv)
|
125
|
+
opts = {}
|
126
|
+
|
127
|
+
argv.options do |args|
|
128
|
+
args.on '-c', '--concurrency INT', "processor threads to use" do |arg|
|
129
|
+
opts[:concurrency] = Integer(arg)
|
130
|
+
end
|
131
|
+
|
132
|
+
args.on '-d', '--daemon', "Daemonize process" do |arg|
|
133
|
+
opts[:daemon] = arg
|
134
|
+
end
|
135
|
+
|
136
|
+
args.on '-e', '--environment ENV', "Application environment" do |arg|
|
137
|
+
opts[:environment] = arg
|
138
|
+
end
|
139
|
+
|
140
|
+
args.on "-v", "--verbose", "Print more verbose output" do |arg|
|
141
|
+
opts[:verbose] = arg
|
142
|
+
end
|
143
|
+
|
144
|
+
args.on '-C', '--config PATH', "path to YAML config file" do |arg|
|
145
|
+
opts[:config_file] = arg
|
146
|
+
end
|
147
|
+
|
148
|
+
args.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
|
149
|
+
opts[:logfile] = arg
|
150
|
+
end
|
151
|
+
|
152
|
+
args.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
|
153
|
+
opts[:pidfile] = arg
|
154
|
+
end
|
155
|
+
|
156
|
+
args.on '-V', '--version', "Print version and exit" do |arg|
|
157
|
+
puts "Chequeo #{Chequeo::VERSION}"
|
158
|
+
exit(0)
|
159
|
+
end
|
160
|
+
|
161
|
+
args.parse!
|
162
|
+
end
|
163
|
+
|
164
|
+
opts
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
def require_system
|
169
|
+
ENV['RACK_ENV'] = ENV['RAILS_ENV']
|
170
|
+
|
171
|
+
require 'rails'
|
172
|
+
|
173
|
+
require File.expand_path("./config/application.rb")
|
174
|
+
require File.expand_path("./config/environment.rb")
|
175
|
+
|
176
|
+
end
|
177
|
+
|
178
|
+
def write_pid
|
179
|
+
if path = options[:pidfile]
|
180
|
+
pidfile = File.expand_path(path)
|
181
|
+
File.open(pidfile, 'w') do |f|
|
182
|
+
f.puts ::Process.pid
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chequeo
|
4
|
+
class Configuration
|
5
|
+
|
6
|
+
attr_accessor :test, :notifications, :schedules, :logger, :redis, :workers, :dead_man_switch
|
7
|
+
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@test = nil
|
11
|
+
@notifications = []
|
12
|
+
@schedules = []
|
13
|
+
@logger = Logger.new(STDOUT)
|
14
|
+
@logger.level = Logger::WARN
|
15
|
+
@redis = nil
|
16
|
+
@workers ||= 5
|
17
|
+
|
18
|
+
@schedules << Chequeo::ScheduledJob.new("*/5 * * * *", Chequeo::HealthChecks::SystemUp, {})
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def schedule(crontab, checkup_class, options = {})
|
23
|
+
Chequeo.config.logger.debug "Chequeo::Configuration.schedule - Loading #{checkup_class} to run on schedule #{crontab} with options #{options}"
|
24
|
+
@schedules << Chequeo::ScheduledJob.new(crontab, checkup_class, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_notification(notification_class, rules = {}, configuration = {}, options = {})
|
28
|
+
Chequeo.config.logger.debug "Chequeo::Configuration.add_notification - Adding #{notification_class} adding rules #{rules} and configuration #{configuration} with options #{options}"
|
29
|
+
notification = notification_class.new(options)
|
30
|
+
notification.configure(rules, configuration)
|
31
|
+
@notifications << notification
|
32
|
+
end
|
33
|
+
|
34
|
+
def dead_mans_switch(&block)
|
35
|
+
@dead_man_switch = block
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'oj'
|
4
|
+
|
5
|
+
module Chequeo
|
6
|
+
module HealthChecks
|
7
|
+
class Base
|
8
|
+
|
9
|
+
attr_accessor :jid, :enqueue_time, :type, :errors, :warnings, :completion_text, :rules
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@enqueue_time = Time.now.to_i
|
13
|
+
@jid = Digest::SHA1.hexdigest("#{self.class.name}:#{options.to_s}:#{@enqueue_time}")
|
14
|
+
@type = self.class.name
|
15
|
+
@errors = []
|
16
|
+
@warnings = []
|
17
|
+
@completion_text = nil
|
18
|
+
@rules = options.has_key?(:rules) ? options[:rules] : {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def process
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
def notify
|
26
|
+
Chequeo.config.notifications.each do |notification|
|
27
|
+
notification.send_notifications(self)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_job_title
|
32
|
+
"Chequeo Job ##{@jid} - #{self.class.name.demodulize}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_short_job_title
|
36
|
+
"[Chequeo] [#{self.class.name.demodulize}] ##{@jid} - "
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_text
|
40
|
+
val = "Chequeo Job ##{@jid} for type #{self.class.name.demodulize} completed"
|
41
|
+
val += "\n\n#{@completion_text}" if @completion_text
|
42
|
+
val
|
43
|
+
end
|
44
|
+
|
45
|
+
def serialize
|
46
|
+
Oj.dump(self)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.deserialize(data)
|
50
|
+
parsed_obj = Oj.load(data)
|
51
|
+
parsed_obj
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Chequeo
|
5
|
+
module HealthChecks
|
6
|
+
class SystemUp < Base
|
7
|
+
|
8
|
+
def process
|
9
|
+
Chequeo.config.dead_man_switch.call if Chequeo.config.dead_man_switch
|
10
|
+
end
|
11
|
+
|
12
|
+
def notify
|
13
|
+
return #do not notify since we are going to hit the endpoint for checkins
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chequeo
|
4
|
+
class Manager
|
5
|
+
|
6
|
+
attr_reader :workers
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
Chequeo.config.logger.debug "Creating Worker Pool"
|
10
|
+
@workers ||= []
|
11
|
+
|
12
|
+
Chequeo.config.workers.times do
|
13
|
+
@workers << Chequeo::CheckupProcessor.new
|
14
|
+
end
|
15
|
+
Chequeo.config.logger.debug "Creating Worker Pool"
|
16
|
+
end
|
17
|
+
|
18
|
+
def process
|
19
|
+
Chequeo.config.logger.debug "Starting Worker Pool"
|
20
|
+
@workers.each do |w|
|
21
|
+
w.process
|
22
|
+
end
|
23
|
+
Chequeo.config.logger.debug "Starting Worker Pool"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chequeo
|
4
|
+
module Notifications
|
5
|
+
class Base
|
6
|
+
|
7
|
+
attr_accessor :rules, :configuration
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@rules = {:on_completion => true, :warning_threshold => 10, :error_threshold => 10}
|
11
|
+
@configuration = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def configure(rules = {}, config = {})
|
15
|
+
@rules.merge!(rules)
|
16
|
+
@configuration.merge!(config)
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def send_notifications( job , job_rules = {})
|
24
|
+
return unless valid?
|
25
|
+
_rules = @rules.merge(job_rules) #merge in job specific overrides
|
26
|
+
send_warnings(job) if job.warnings.count > 0 && job.warnings.count >= ( _rules[:warning_threshold] || 0 )
|
27
|
+
send_errors(job) if job.errors.count > 0 && job.errors.count >= ( _rules[:error_threshold] || 0 )
|
28
|
+
send_on_completion(job) if _rules[:on_completion]
|
29
|
+
end
|
30
|
+
|
31
|
+
def send_warnings
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
def send_errors
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
def send_on_completion
|
40
|
+
return
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chequeo/notifications/base'
|
4
|
+
|
5
|
+
module Chequeo
|
6
|
+
module Notifications
|
7
|
+
class Logger < Base
|
8
|
+
|
9
|
+
#
|
10
|
+
# configuration: {
|
11
|
+
# logger: LOGGER_INSTANCE
|
12
|
+
# }
|
13
|
+
|
14
|
+
def valid?
|
15
|
+
@configuration && @configuration.has_key?(:logger)
|
16
|
+
end
|
17
|
+
|
18
|
+
def send_on_completion(job)
|
19
|
+
_text = job.get_text
|
20
|
+
send_logger_notice(_text)
|
21
|
+
end
|
22
|
+
|
23
|
+
def send_warnings(job)
|
24
|
+
job.warnings.each{|x| send_logger_warning("#{job.get_short_job_title}#{x}") }
|
25
|
+
end
|
26
|
+
|
27
|
+
def send_errors(job)
|
28
|
+
job.errors.each{|x| send_logger_error("#{job.get_short_job_title}#{x}") }
|
29
|
+
end
|
30
|
+
|
31
|
+
def send_logger_warning(text)
|
32
|
+
begin
|
33
|
+
@configuration[:logger].warn(text)
|
34
|
+
rescue => e
|
35
|
+
Chequeo.config.logger.error "Chequeo::Notifications::Logger.send_notifications - #{e.message}"
|
36
|
+
Chequeo.config.logger.error "Chequeo::Notifications::Logger.send_notifications - #{e.backtrace.inspect}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def send_logger_error(text)
|
41
|
+
begin
|
42
|
+
@configuration[:logger].error(text)
|
43
|
+
rescue => e
|
44
|
+
Chequeo.config.logger.error "Chequeo::Notifications::Logger.send_notifications - #{e.message}"
|
45
|
+
Chequeo.config.logger.error "Chequeo::Notifications::Logger.send_notifications - #{e.backtrace.inspect}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def send_logger_notice(text)
|
50
|
+
begin
|
51
|
+
@configuration[:logger].info(text)
|
52
|
+
rescue => e
|
53
|
+
Chequeo.config.logger.error "Chequeo::Notifications::Logger.send_notifications - #{e.message}"
|
54
|
+
Chequeo.config.logger.error "Chequeo::Notifications::Logger.send_notifications - #{e.backtrace.inspect}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chequeo/notifications/base'
|
4
|
+
require 'slack-ruby-client'
|
5
|
+
|
6
|
+
module Chequeo
|
7
|
+
module Notifications
|
8
|
+
class Slack < Base
|
9
|
+
|
10
|
+
#
|
11
|
+
# configuration: {
|
12
|
+
# token: YOUR_SLACK_API_TOKEN,
|
13
|
+
# channel: YOUR_SLACK_CHANNEL_HERE
|
14
|
+
# }
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
@configuration && @configuration.has_key?(:token) && @configuration.has_key?(:channel)
|
18
|
+
end
|
19
|
+
|
20
|
+
def send_on_completion(job)
|
21
|
+
_title = job.get_job_title
|
22
|
+
_text = job.get_text
|
23
|
+
send_message(_text, "good", _title)
|
24
|
+
end
|
25
|
+
|
26
|
+
def send_warnings(job)
|
27
|
+
_title = job.get_job_title
|
28
|
+
_warnings = job.warnings.collect{|x| "- #{x}"}.join("\n")
|
29
|
+
send_message(_warnings, "warning", _title)
|
30
|
+
end
|
31
|
+
|
32
|
+
def send_errors(job)
|
33
|
+
_title = job.get_job_title
|
34
|
+
_errors = job.errors.collect{|x| "- #{x}"}.join("\n")
|
35
|
+
send_message(_errors, "danger", _title)
|
36
|
+
end
|
37
|
+
|
38
|
+
def send_message(text, color, title)
|
39
|
+
begin
|
40
|
+
client = ::Slack::Web::Client.new(token: @configuration[:token])
|
41
|
+
client.chat_postMessage(
|
42
|
+
channel: "#{@configuration[:channel]}",
|
43
|
+
as_user: true,
|
44
|
+
attachments: [
|
45
|
+
{
|
46
|
+
fallback: "Chequeo has posted a notification to slack.",
|
47
|
+
title: title,
|
48
|
+
text: text,
|
49
|
+
color: color
|
50
|
+
}
|
51
|
+
]
|
52
|
+
)
|
53
|
+
rescue => e
|
54
|
+
Chequeo.config.logger.error "Chequeo::Notifications::Slack.send_notifications - #{e.message}"
|
55
|
+
Chequeo.config.logger.error "Chequeo::Notifications::Slack.send_notifications - #{e.backtrace.inspect}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chequeo
|
4
|
+
class Processor
|
5
|
+
|
6
|
+
attr_reader :thread
|
7
|
+
|
8
|
+
def initialize #(mgr)
|
9
|
+
Chequeo.config.logger.debug "Start Chequeo::Processor.initialize"
|
10
|
+
#@mgr = mgr
|
11
|
+
@thread = nil
|
12
|
+
@completed = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def process
|
16
|
+
@thread ||= Thread.new {
|
17
|
+
begin
|
18
|
+
process_task
|
19
|
+
rescue => e
|
20
|
+
Chequeo.config.logger.error "Chequeo::Scheduler.process - #{e.message}"
|
21
|
+
Chequeo.config.logger.error "Chequeo::Scheduler.process - #{e.backtrace.inspect}"
|
22
|
+
end
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def process_task
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chequeo
|
4
|
+
class ScheduledJob
|
5
|
+
|
6
|
+
attr_accessor :cron, :klass, :options, :jid
|
7
|
+
|
8
|
+
def initialize(cron, klass, options = {})
|
9
|
+
@options = options
|
10
|
+
@cron = cron
|
11
|
+
@klass = klass
|
12
|
+
@jid = Digest::SHA1.hexdigest("#{cron.to_s}:#{klass.to_s}:#{options.to_s}")
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_klass
|
16
|
+
klass #TODO: Handle if klass is a string
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_job_run_id job_time
|
20
|
+
{ job_time: _time, job_id: Digest::SHA1.hexdigest("#{cron.to_s}:#{klass.to_s}:#{options.to_s}:#{job_time}") }
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'galactic-senate'
|
4
|
+
require 'fugit'
|
5
|
+
|
6
|
+
module Chequeo
|
7
|
+
class Scheduler < Chequeo::Processor
|
8
|
+
def initialize(mgr)
|
9
|
+
Chequeo.config.logger.debug "Start Chequeo::Scheduler.initialize"
|
10
|
+
|
11
|
+
@leader = false
|
12
|
+
|
13
|
+
GalacticSenate.configure do |config|
|
14
|
+
config.redis = Chequeo.config.redis
|
15
|
+
config.logger = Chequeo.config.logger
|
16
|
+
|
17
|
+
config.on(:elected) do
|
18
|
+
Rails.logger.warn "I was just elected!!"
|
19
|
+
@leader = true
|
20
|
+
end
|
21
|
+
|
22
|
+
config.on(:ousted) do
|
23
|
+
Rails.logger.warn "I was just ousted!!"
|
24
|
+
@leader = false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Chequeo.config.logger.debug "Starting the senate"
|
29
|
+
|
30
|
+
GalacticSenate::Delegation.instance.debate
|
31
|
+
|
32
|
+
Chequeo.config.logger.debug "Senate Started"
|
33
|
+
|
34
|
+
Chequeo.config.redis.del("chequeo-jobs")
|
35
|
+
|
36
|
+
Chequeo.config.schedules.each do |element|
|
37
|
+
Chequeo.config.redis.sadd("chequeo-jobs", element.jid)
|
38
|
+
end
|
39
|
+
|
40
|
+
Chequeo.config.logger.debug "End Chequeo::Scheduler.initialize"
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
def process
|
46
|
+
Chequeo.config.logger.debug "Start Chequeo::Scheduler.process"
|
47
|
+
|
48
|
+
timer_task = Concurrent::TimerTask.new(execution_interval: 30) do |task|
|
49
|
+
|
50
|
+
begin
|
51
|
+
process_task
|
52
|
+
rescue => e
|
53
|
+
Chequeo.config.logger.error "Chequeo::Scheduler.process - #{e.message}"
|
54
|
+
Chequeo.config.logger.error "Chequeo::Scheduler.process - #{e.backtrace.inspect}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
timer_task.execute
|
59
|
+
Chequeo.config.logger.debug "End Chequeo::Scheduler.process"
|
60
|
+
end
|
61
|
+
|
62
|
+
def process_task
|
63
|
+
Chequeo.config.logger.debug "@leader = #{@leader}"
|
64
|
+
return unless @leader
|
65
|
+
|
66
|
+
Chequeo.config.schedules.each do |element|
|
67
|
+
_tempest_fugit = Fugit::Cron.parse(element.cron)
|
68
|
+
|
69
|
+
_last_element = Chequeo.config.redis.zrange("chequeo-job-#{element.jid}", -1, -1, :with_scores => true)
|
70
|
+
_last_element_score = !_last_element.empty? ? _last_element[0][1] : -1
|
71
|
+
|
72
|
+
#puts "next = #{_tempest_fugit.next_time.to_i} - previous = #{_tempest_fugit.previous_time.to_i} - last = #{_last_element_score}"
|
73
|
+
if _last_element_score <= _tempest_fugit.previous_time.to_i
|
74
|
+
|
75
|
+
_job = element.klass.new(element.options)
|
76
|
+
|
77
|
+
Chequeo.config.logger.debug "Chequeo::Scheduler::process_task - Queueing #{_job.jid} of type #{_job.class.name}"
|
78
|
+
|
79
|
+
Chequeo.config.redis.lpush("chequeo-job-queue", _job.serialize)
|
80
|
+
Chequeo.config.redis.zadd("chequeo-job-#{element.jid}", _job.enqueue_time, _job.jid)
|
81
|
+
Chequeo.config.redis.zremrangebyrank("chequeo-job-#{element.jid}", 0, -26)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
|
metadata
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: chequeo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0.beta
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- jdejong
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-09-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: twilio-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '5.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: slack-ruby-client
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 0.9.0
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.9.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: fugit
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.1.5
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 1.1.5
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: concurrent-ruby
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.0'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: galactic-senate
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0.1'
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0.1'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: oj
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.6'
|
96
|
+
- - "<"
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '4'
|
99
|
+
type: :runtime
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '3.6'
|
106
|
+
- - "<"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '4'
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: redis
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '4.0'
|
116
|
+
type: :runtime
|
117
|
+
prerelease: false
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '4.0'
|
123
|
+
- !ruby/object:Gem::Dependency
|
124
|
+
name: rake
|
125
|
+
requirement: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - "~>"
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '10'
|
130
|
+
type: :development
|
131
|
+
prerelease: false
|
132
|
+
version_requirements: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - "~>"
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '10'
|
137
|
+
- !ruby/object:Gem::Dependency
|
138
|
+
name: minitest
|
139
|
+
requirement: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - "~>"
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '5.3'
|
144
|
+
type: :development
|
145
|
+
prerelease: false
|
146
|
+
version_requirements: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - "~>"
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '5.3'
|
151
|
+
- !ruby/object:Gem::Dependency
|
152
|
+
name: bundler
|
153
|
+
requirement: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - "~>"
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '1.15'
|
158
|
+
type: :development
|
159
|
+
prerelease: false
|
160
|
+
version_requirements: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - "~>"
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '1.15'
|
165
|
+
description: A framework to make running checkups on your platform easier.
|
166
|
+
email:
|
167
|
+
- ''
|
168
|
+
executables:
|
169
|
+
- chequeo
|
170
|
+
extensions: []
|
171
|
+
extra_rdoc_files: []
|
172
|
+
files:
|
173
|
+
- ".gitignore"
|
174
|
+
- CHANGELOG.md
|
175
|
+
- Gemfile
|
176
|
+
- LICENSE
|
177
|
+
- README.md
|
178
|
+
- Rakefile
|
179
|
+
- bin/chequeo
|
180
|
+
- chequeo.gemspec
|
181
|
+
- init.rb
|
182
|
+
- lib/chequeo.rb
|
183
|
+
- lib/chequeo/checkup_processor.rb
|
184
|
+
- lib/chequeo/cli.rb
|
185
|
+
- lib/chequeo/configuration.rb
|
186
|
+
- lib/chequeo/healthchecks/base.rb
|
187
|
+
- lib/chequeo/healthchecks/model_validity.rb
|
188
|
+
- lib/chequeo/healthchecks/system_up.rb
|
189
|
+
- lib/chequeo/healthchecks/test_check.rb
|
190
|
+
- lib/chequeo/manager.rb
|
191
|
+
- lib/chequeo/notifications/base.rb
|
192
|
+
- lib/chequeo/notifications/logger.rb
|
193
|
+
- lib/chequeo/notifications/slack.rb
|
194
|
+
- lib/chequeo/notifications/twilio.rb
|
195
|
+
- lib/chequeo/notifications/webhook.rb
|
196
|
+
- lib/chequeo/processor.rb
|
197
|
+
- lib/chequeo/scheduled_job.rb
|
198
|
+
- lib/chequeo/scheduler.rb
|
199
|
+
- lib/chequeo/version.rb
|
200
|
+
- lib/generators/chequeo/install_generator.rb
|
201
|
+
- lib/generators/chequeo/templates/initializer.rb
|
202
|
+
homepage: https://github.com/jdejong/chequeo
|
203
|
+
licenses:
|
204
|
+
- MIT
|
205
|
+
metadata: {}
|
206
|
+
post_install_message:
|
207
|
+
rdoc_options: []
|
208
|
+
require_paths:
|
209
|
+
- lib
|
210
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
211
|
+
requirements:
|
212
|
+
- - ">="
|
213
|
+
- !ruby/object:Gem::Version
|
214
|
+
version: 2.2.0
|
215
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
216
|
+
requirements:
|
217
|
+
- - ">"
|
218
|
+
- !ruby/object:Gem::Version
|
219
|
+
version: 1.3.1
|
220
|
+
requirements: []
|
221
|
+
rubyforge_project:
|
222
|
+
rubygems_version: 2.4.8
|
223
|
+
signing_key:
|
224
|
+
specification_version: 4
|
225
|
+
summary: Checkups Made Easy
|
226
|
+
test_files: []
|