checkups 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4365201dccdda439c272becff16a31e9ace9e8eb924b20c63ae74b58e3430ee6
4
+ data.tar.gz: 03bdf297ce7b9f876dd777f6016ddeecb747b91b4a7c313481f44e2167f47546
5
+ SHA512:
6
+ metadata.gz: c777b45a095f2597d937cbd42e80bd6ed21a9403155b844c3a3e31a1338cb2a723578c041d7d278d39ea4ded0aadacc14c937fd9fc861fc10911a0f50e3f9bd0
7
+ data.tar.gz: 1d657c9f5d375c3fdf1a7efdab01bb5b6515ea04a59cf3a585cc45d8033c5651589f3a6d1ca69495b19e7b198ae833c1c23dd924131ec2f4ba95046410f44a6b
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /bin/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ Gemfile.lock
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,104 @@
1
+ require:
2
+ - rubocop-performance
3
+ # - rubocop_lineup
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.5
7
+ NewCops: disable
8
+
9
+ Layout/ClassStructure:
10
+ Enabled: true
11
+ Categories:
12
+ module_inclusion:
13
+ - include
14
+ - prepend
15
+ - extend
16
+ ExpectedOrder:
17
+ - module_inclusion
18
+ - constants
19
+ - public_class_methods
20
+ - initializer
21
+ - public_methods
22
+ - protected_methods
23
+ - private_methods
24
+
25
+ Layout/DotPosition:
26
+ EnforcedStyle: trailing
27
+
28
+ Layout/LineLength:
29
+ Enabled: true
30
+ Max: 120
31
+ AllowHeredoc: true
32
+ AllowURI: true
33
+ URISchemes: http, https
34
+
35
+ Layout/MultilineMethodCallIndentation:
36
+ Enabled: false
37
+
38
+ Layout/SpaceInsideHashLiteralBraces:
39
+ Enabled: false
40
+
41
+ Metrics:
42
+ Enabled: true
43
+
44
+ Metrics/AbcSize:
45
+ Max: 20
46
+
47
+ Metrics/BlockLength:
48
+ Exclude:
49
+ - 'Rakefile'
50
+ - '**/*.rake'
51
+ - 'spec/**/*.rb'
52
+ ExcludedMethods: ['included', 'namespace']
53
+
54
+ Metrics/ClassLength:
55
+ Exclude:
56
+ - 'Rakefile'
57
+ - 'spec/**/*.rb'
58
+
59
+ Metrics/MethodLength:
60
+ CountComments: false # count full line comments?
61
+ Max: 15
62
+
63
+ Naming/MemoizedInstanceVariableName:
64
+ Enabled: false
65
+
66
+ Style/BlockComments:
67
+ Enabled: false
68
+
69
+ Style/Documentation:
70
+ Enabled: false
71
+
72
+ Style/DocumentationMethod:
73
+ Enabled: false
74
+
75
+ Style/FrozenStringLiteralComment:
76
+ EnforcedStyle: always_true
77
+ Enabled: true
78
+
79
+ Style/GuardClause:
80
+ Enabled: false
81
+
82
+ Style/MultilineBlockChain:
83
+ Enabled: false
84
+
85
+ Style/ParallelAssignment:
86
+ Enabled: false
87
+
88
+ Style/RedundantSelf:
89
+ Enabled: false
90
+
91
+ Style/StringLiterals:
92
+ EnforcedStyle: double_quotes
93
+
94
+ Style/TrailingCommaInArrayLiteral:
95
+ Enabled: false
96
+
97
+ Style/TrailingCommaInHashLiteral:
98
+ Enabled: false
99
+
100
+ Style/WhenThen:
101
+ Enabled: true
102
+
103
+ Style/WordArray:
104
+ Enabled: false
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.6.5
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in checkups.gemspec
6
+ gemspec
7
+
8
+ gem "mocha"
9
+ gem "rake", "~> 12.0"
10
+ gem "rspec", "~> 3.0"
11
+ gem "rubocop"
12
+ gem "rubocop_lineup"
13
+ gem "rubocop-performance"
14
+ gem "timecop"
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Mystery Organization, Inc
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,60 @@
1
+ # Checkups
2
+
3
+ ## Status: Jul 2020
4
+
5
+ Ryan Laughlin gave a [great talk at RailsConf
6
+ 2018](https://www.youtube.com/watch?v=gEAlhKaK2I4) introducing our team to this
7
+ concept of production checkups. Robb ran with it and I (chrismo :waves:) helped
8
+ a little along the way. We've released what we have out to the internets in case
9
+ anyone else wants to help out as well.
10
+
11
+ As of our initial version here (0.9.0), we've got the core code in place, and
12
+ some half-arsed Slack (for notifications) and Sidekiq integration (for custom
13
+ worker checks). The goal for a 1.0 release is to figure out the proper way to
14
+ handle 3rd party integrations.
15
+
16
+ We use this gem in production at mysteryscience.com and this code has been live
17
+ there for over a year.
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'checkups'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ $ bundle install
30
+
31
+ Or install it yourself as:
32
+
33
+ $ gem install checkups
34
+
35
+ ## Usage
36
+
37
+ TODO: Write usage instructions here
38
+
39
+ ## Development
40
+
41
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
42
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
43
+ prompt that will allow you to experiment.
44
+
45
+ To install this gem onto your local machine, run `bundle exec rake install`. To
46
+ release a new version, update the version number in `version.rb`, and then run
47
+ `bundle exec rake release`, which will create a git tag for the version, push
48
+ git commits and tags, and push the `.gem` file to
49
+ [rubygems.org](https://rubygems.org).
50
+
51
+ ## Contributing
52
+
53
+ Bug reports and pull requests are welcome on GitHub at
54
+ https://github.com/mysterysci/checkups.
55
+
56
+
57
+ ## License
58
+
59
+ The gem is available as open source under the terms of the [MIT
60
+ License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+
10
+ require "rubocop/rake_task"
11
+
12
+ RuboCop::RakeTask.new(:rubocop)
13
+
14
+ task default: :rubocop
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/checkups/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "checkups"
7
+ spec.version = Checkups::VERSION
8
+ spec.authors = %w[robbprescott chrismo]
9
+ spec.email = %w[robb@prescott.dev chrismo@clabs.org]
10
+
11
+ spec.summary = "Runtime checks on production conditions."
12
+ spec.homepage = "https://github.com/mysterysci/checkups"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/mysterysci/checkups"
18
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ # spec.bindir = "exe"
26
+ # spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ require "checkups/configuration"
6
+ require "checkups/notification_timer"
7
+ require "checkups/checkup"
8
+ require "checkups/performance"
9
+ require "checkups/slack_notifier"
10
+ require "checkups/version"
11
+ require "checkups/sidekiq_worker"
12
+
13
+ module Checkups
14
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: flesh out a manager class that can keep better track of the Checkup classes
4
+ # TODO: \ and if they're being executed, so if something gets skipped for some reason
5
+ # TODO: \ maybe it has a chance of being discovered instead of silently just never
6
+ # TODO: \ being executed.
7
+ module Checkups
8
+ class Checkup
9
+ attr_reader :status_message, :status, :verbose, :name, :url
10
+
11
+ def self.hourly
12
+ checkups_by_frequency(:hourly)
13
+ end
14
+
15
+ def self.daily
16
+ checkups_by_frequency(:daily)
17
+ end
18
+
19
+ # TODO: Remove verbose from method signature
20
+ def self.checkups_by_frequency(frequency, _verbose: false)
21
+ checkup_classes = ObjectSpace.each_object(::Class).select { |klass| klass < self }
22
+ checkup_classes.select { |klass| klass.frequency == frequency }
23
+ end
24
+
25
+ def self.frequency
26
+ :never
27
+ end
28
+
29
+ def initialize(verbose: false)
30
+ @notify_frequency = :always
31
+ @verbose = verbose
32
+ @name = nil
33
+ ok
34
+ end
35
+
36
+ def check_and_notify!(&block)
37
+ check(&block)
38
+ send_notification
39
+ rescue => e # rubocop:disable Style/RescueStandardError
40
+ handle_error(e)
41
+ end
42
+
43
+ def passed?
44
+ case check
45
+ when nil, :ok, :info
46
+ true
47
+ when :warning, :error
48
+ false
49
+ else
50
+ raise "Unknown status: #{check}"
51
+ end
52
+ end
53
+
54
+ def check
55
+ ok
56
+ end
57
+
58
+ def notify_message
59
+ "Checkup #{@status.to_s.capitalize}: " + @status_message
60
+ end
61
+
62
+ protected
63
+
64
+ def ok
65
+ set_status(:ok, nil)
66
+ end
67
+
68
+ def info(message)
69
+ set_status(:info, message)
70
+ end
71
+
72
+ def warning(message)
73
+ set_status(:warning, message)
74
+ end
75
+ alias warn warning
76
+
77
+ def error(message)
78
+ set_status(:error, message)
79
+ end
80
+
81
+ def send_notification
82
+ puts "#{status}#{status_message ? ': ' + status_message : ''}" if verbose
83
+
84
+ notifier.notify(self) if need_notification?
85
+ end
86
+
87
+ def notifier
88
+ @notifier ||= Checkups.configuration.notifier
89
+ end
90
+
91
+ def logger
92
+ @logger ||= Checkups.configuration.logger
93
+ end
94
+
95
+ def need_notification?
96
+ @status_message && NotificationTimer.new(@notify_frequency, @status).now?
97
+ end
98
+
99
+ def set_status(status, message)
100
+ @status_message = message
101
+ @status = status
102
+ end
103
+
104
+ # rubocop:disable Style/RescueStandardError
105
+ def handle_error(error)
106
+ message = "Error running Checkup class #{self.class.name}: #{error.message} #{error.backtrace[0]}"
107
+ set_status(:fatal, message)
108
+ notifier.notify(self)
109
+ rescue
110
+ # Last chance for gas
111
+ logger.error("Checkup handle_error error! #{error.message}") rescue nil # rubocop:disable Style/RescueModifier
112
+ end
113
+ # rubocop:enable Style/RescueStandardError
114
+ end
115
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkups
4
+ class << self
5
+ attr_writer :configuration
6
+
7
+ def configuration
8
+ @configuration ||= Configuration.new
9
+ end
10
+ end
11
+
12
+ def self.configure
13
+ yield(configuration)
14
+ end
15
+
16
+ class Configuration
17
+ attr_accessor :notifier, :logger
18
+
19
+ def initialize
20
+ @notifier = Checkups::SlackNotifier.new
21
+ @logger = Logger.new(IO::NULL)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkups
4
+ class NotificationTimer
5
+ def initialize(frequency, status)
6
+ @frequency = frequency
7
+ @status = status
8
+ end
9
+
10
+ def now?
11
+ case @status
12
+ when :warning, :error
13
+ true
14
+ else
15
+ case @frequency
16
+ when :always
17
+ true
18
+ when /times_a_day/
19
+ word = @frequency.to_s.scan(/(.*)_times_a_day/).join
20
+ IntervalChecker.is_hour_at_day_interval?(word)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ class IntervalChecker
27
+ # rubocop:disable Naming/PredicateName
28
+ def self.is_hour_at_day_interval?(daily_frequency, time = Time.now.utc)
29
+ frequency = word_to_number(daily_frequency) || 1
30
+ hourly_interval = 24 / frequency
31
+ time.utc.hour.divmod(hourly_interval)[1].zero?
32
+ end
33
+ # rubocop:enable Naming/PredicateName
34
+
35
+ def self.word_to_number(word)
36
+ case word
37
+ when String
38
+ {
39
+ "two" => 2,
40
+ "three" => 3,
41
+ "four" => 4,
42
+ "six" => 6,
43
+ "eight" => 8,
44
+ }[word]
45
+ else
46
+ word
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkups
4
+ class Performance < Checkup
5
+ def self.frequency
6
+ :never
7
+ end
8
+
9
+ attr_reader :error_limit, :warning_limit
10
+
11
+ def initialize(verbose: false, name: nil, warning_limit: 1800, error_limit: 3600)
12
+ super(verbose: verbose)
13
+ @name = name
14
+ @warning_limit = warning_limit
15
+ @error_limit = error_limit
16
+ ok
17
+ end
18
+
19
+ def check
20
+ start_time = Time.now.to_i
21
+ yield
22
+ check_elapsed(Time.now.to_i - start_time)
23
+ end
24
+
25
+ def check_elapsed(elapsed)
26
+ if warning_limit <= elapsed && elapsed < error_limit
27
+ warning "#{name} took longer than #{warning_limit}s: #{elapsed}s"
28
+ elsif error_limit <= elapsed
29
+ error "#{name} took longer than #{error_limit}s: #{elapsed}s"
30
+ else
31
+ ok
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Sidekiq)
4
+ require "sidekiq"
5
+ require "active_support/core_ext/string/inflections"
6
+
7
+ module Checkups
8
+ class SidekiqWorker
9
+ include Sidekiq::Worker
10
+
11
+ sidekiq_options retry: 3
12
+
13
+ # :hourly or :daily, spread them out over time
14
+ def perform(frequency, verbose = false)
15
+ Checkup.checkups_by_frequency(frequency.to_sym).each_with_index do |klass, i|
16
+ OneCheckupWorker.perform_in(i * 10, klass.name, verbose)
17
+ end
18
+ end
19
+ end
20
+
21
+ class OneCheckupWorker
22
+ include Sidekiq::Worker
23
+
24
+ sidekiq_options retry: 3
25
+
26
+ # :hourly or :daily
27
+ def perform(klass, verbose = false)
28
+ klass.constantize.new(verbose: verbose).tap do |checkup|
29
+ printf "Checkup: #{klass}..." if verbose
30
+ checkup.check_and_notify!
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkups
4
+ class SlackNotifier
5
+ def notify(checkup)
6
+ attachments = build_attachments(checkup.status,
7
+ checkup.notify_message,
8
+ checkup.name,
9
+ checkup.url)
10
+ send_attachments(attachments)
11
+ end
12
+
13
+ def send_attachments(_attachments)
14
+ raise "Must subclass Checkups::SlackNotifier#send_attachments"
15
+ end
16
+
17
+ # https://api.slack.com/docs/message-attachments#attachment_structure
18
+ def build_attachments(status, message, title = nil, title_link = nil)
19
+ attachment = {"color": status_to_slack_color(status),
20
+ "text": message}
21
+ attachment[:title] = title if title
22
+ attachment[:title_link] = title_link if title_link
23
+ [attachment]
24
+ end
25
+
26
+ def status_to_slack_color(status)
27
+ case status
28
+ when :ok, :info
29
+ "good"
30
+ when :warning
31
+ "warning"
32
+ when :error, :fatal
33
+ "danger"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkups
4
+ VERSION = "0.9.0"
5
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: checkups
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - robbprescott
8
+ - chrismo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2020-07-27 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description:
15
+ email:
16
+ - robb@prescott.dev
17
+ - chrismo@clabs.org
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - ".gitignore"
23
+ - ".rspec"
24
+ - ".rubocop.yml"
25
+ - ".travis.yml"
26
+ - Gemfile
27
+ - LICENSE.txt
28
+ - README.md
29
+ - Rakefile
30
+ - bin/console
31
+ - checkups.gemspec
32
+ - lib/checkups.rb
33
+ - lib/checkups/checkup.rb
34
+ - lib/checkups/configuration.rb
35
+ - lib/checkups/notification_timer.rb
36
+ - lib/checkups/performance.rb
37
+ - lib/checkups/sidekiq_worker.rb
38
+ - lib/checkups/slack_notifier.rb
39
+ - lib/checkups/version.rb
40
+ homepage: https://github.com/mysterysci/checkups
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ homepage_uri: https://github.com/mysterysci/checkups
45
+ source_code_uri: https://github.com/mysterysci/checkups
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.3.0
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.1.4
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Runtime checks on production conditions.
65
+ test_files: []