vitals_monitor 0.1.0
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/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +273 -0
- data/LICENSE.txt +21 -0
- data/README.md +181 -0
- data/Rakefile +15 -0
- data/app/controllers/vitals_monitor/vitals_controller.rb +95 -0
- data/app/views/vitals_monitor/vitals/index.html.erb +85 -0
- data/app/views/vitals_monitor/vitals/show.html.erb +68 -0
- data/config/initializers/vitals_monitor.rb +12 -0
- data/config/routes.rb +6 -0
- data/lib/tasks/vitals_monitor.rake +93 -0
- data/lib/vitals_monitor/checks/base.rb +33 -0
- data/lib/vitals_monitor/checks/postgres.rb +17 -0
- data/lib/vitals_monitor/checks/redis.rb +31 -0
- data/lib/vitals_monitor/checks/sidekiq.rb +24 -0
- data/lib/vitals_monitor/configuration.rb +27 -0
- data/lib/vitals_monitor/engine.rb +14 -0
- data/lib/vitals_monitor/formatters/json.rb +27 -0
- data/lib/vitals_monitor/version.rb +5 -0
- data/lib/vitals_monitor.rb +27 -0
- data/sig/vitals_monitor.rbs +52 -0
- data/vitals_monitor.gemspec +40 -0
- metadata +131 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VitalsMonitor
|
|
4
|
+
class VitalsController < ActionController::Base
|
|
5
|
+
before_action :set_format
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@results = check_all_components
|
|
9
|
+
@overall_status = overall_status(@results)
|
|
10
|
+
status_code = @overall_status == :unhealthy ? 503 : 200
|
|
11
|
+
|
|
12
|
+
respond_to do |format|
|
|
13
|
+
format.html { render status: status_code }
|
|
14
|
+
format.json { render json: format_json_response(@results, @overall_status), status: status_code }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def show
|
|
19
|
+
component = params[:component].to_sym
|
|
20
|
+
|
|
21
|
+
unless VitalsMonitor.config.enabled?(component)
|
|
22
|
+
respond_to do |format|
|
|
23
|
+
format.html { render status: 404 }
|
|
24
|
+
format.json { render json: { error: "Component #{component} is not enabled" }, status: 404 }
|
|
25
|
+
end
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
result = check_component(component)
|
|
30
|
+
@result = result
|
|
31
|
+
@component = component
|
|
32
|
+
@status = result[:status]
|
|
33
|
+
status_code = result[:status] == :unhealthy ? 503 : 200
|
|
34
|
+
|
|
35
|
+
respond_to do |format|
|
|
36
|
+
format.html { render status: status_code }
|
|
37
|
+
format.json { render json: format_single_json_response(component, result), status: status_code }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def set_format
|
|
44
|
+
request.format = :html unless request.format.json?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check_all_components
|
|
48
|
+
results = {}
|
|
49
|
+
%i[postgres redis sidekiq].each do |component|
|
|
50
|
+
results[component] = check_component(component) if VitalsMonitor.config.enabled?(component)
|
|
51
|
+
end
|
|
52
|
+
results
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_component(component)
|
|
56
|
+
case component
|
|
57
|
+
when :postgres
|
|
58
|
+
VitalsMonitor::Checks::Postgres.new.check
|
|
59
|
+
when :redis
|
|
60
|
+
VitalsMonitor::Checks::Redis.new.check
|
|
61
|
+
when :sidekiq
|
|
62
|
+
VitalsMonitor::Checks::Sidekiq.new.check
|
|
63
|
+
else
|
|
64
|
+
{ status: :unhealthy, message: "Unknown component: #{component}" }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def overall_status(results)
|
|
69
|
+
return :unhealthy if results.empty?
|
|
70
|
+
return :unhealthy if results.values.any? { |r| r[:status] == :unhealthy }
|
|
71
|
+
|
|
72
|
+
:healthy
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def format_json_response(results, overall_status)
|
|
76
|
+
{
|
|
77
|
+
status: overall_status,
|
|
78
|
+
components: results.transform_values do |result|
|
|
79
|
+
{
|
|
80
|
+
status: result[:status],
|
|
81
|
+
message: result[:message]
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def format_single_json_response(component, result)
|
|
88
|
+
{
|
|
89
|
+
component: component.to_s,
|
|
90
|
+
status: result[:status],
|
|
91
|
+
message: result[:message]
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Vitals Monitor - System Health</title>
|
|
5
|
+
<style>
|
|
6
|
+
body {
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
8
|
+
max-width: 800px;
|
|
9
|
+
margin: 50px auto;
|
|
10
|
+
padding: 20px;
|
|
11
|
+
background-color: #f5f5f5;
|
|
12
|
+
}
|
|
13
|
+
h1 {
|
|
14
|
+
color: #333;
|
|
15
|
+
border-bottom: 2px solid #ddd;
|
|
16
|
+
padding-bottom: 10px;
|
|
17
|
+
}
|
|
18
|
+
.component {
|
|
19
|
+
background: white;
|
|
20
|
+
padding: 15px;
|
|
21
|
+
margin: 10px 0;
|
|
22
|
+
border-radius: 5px;
|
|
23
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
24
|
+
}
|
|
25
|
+
.component-name {
|
|
26
|
+
font-weight: bold;
|
|
27
|
+
font-size: 1.2em;
|
|
28
|
+
margin-bottom: 5px;
|
|
29
|
+
}
|
|
30
|
+
.status-healthy {
|
|
31
|
+
color: #28a745;
|
|
32
|
+
}
|
|
33
|
+
.status-unhealthy {
|
|
34
|
+
color: #dc3545;
|
|
35
|
+
}
|
|
36
|
+
.overall-status {
|
|
37
|
+
padding: 15px;
|
|
38
|
+
margin: 20px 0;
|
|
39
|
+
border-radius: 5px;
|
|
40
|
+
font-size: 1.3em;
|
|
41
|
+
font-weight: bold;
|
|
42
|
+
}
|
|
43
|
+
.overall-healthy {
|
|
44
|
+
background-color: #d4edda;
|
|
45
|
+
color: #155724;
|
|
46
|
+
border: 1px solid #c3e6cb;
|
|
47
|
+
}
|
|
48
|
+
.overall-unhealthy {
|
|
49
|
+
background-color: #f8d7da;
|
|
50
|
+
color: #721c24;
|
|
51
|
+
border: 1px solid #f5c6cb;
|
|
52
|
+
}
|
|
53
|
+
.message {
|
|
54
|
+
color: #666;
|
|
55
|
+
font-size: 0.9em;
|
|
56
|
+
margin-top: 5px;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<h1>System Health Status</h1>
|
|
62
|
+
|
|
63
|
+
<div class="overall-status <%= @overall_status == :healthy ? 'overall-healthy' : 'overall-unhealthy' %>">
|
|
64
|
+
Overall Status: <span class="status-<%= @overall_status %>"><%= @overall_status.to_s.upcase %></span>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<% if @results.empty? %>
|
|
68
|
+
<div class="component">
|
|
69
|
+
<p>No components are enabled.</p>
|
|
70
|
+
</div>
|
|
71
|
+
<% else %>
|
|
72
|
+
<% @results.each do |component, result| %>
|
|
73
|
+
<div class="component">
|
|
74
|
+
<div class="component-name">
|
|
75
|
+
<%= component.to_s.capitalize %>:
|
|
76
|
+
<span class="status-<%= result[:status] %>"><%= result[:status].to_s.upcase %></span>
|
|
77
|
+
</div>
|
|
78
|
+
<% if result[:message] %>
|
|
79
|
+
<div class="message"><%= result[:message] %></div>
|
|
80
|
+
<% end %>
|
|
81
|
+
</div>
|
|
82
|
+
<% end %>
|
|
83
|
+
<% end %>
|
|
84
|
+
</body>
|
|
85
|
+
</html>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Vitals Monitor - <%= @component.to_s.capitalize %> Status</title>
|
|
5
|
+
<style>
|
|
6
|
+
body {
|
|
7
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
8
|
+
max-width: 600px;
|
|
9
|
+
margin: 50px auto;
|
|
10
|
+
padding: 20px;
|
|
11
|
+
background-color: #f5f5f5;
|
|
12
|
+
}
|
|
13
|
+
h1 {
|
|
14
|
+
color: #333;
|
|
15
|
+
border-bottom: 2px solid #ddd;
|
|
16
|
+
padding-bottom: 10px;
|
|
17
|
+
}
|
|
18
|
+
.component {
|
|
19
|
+
background: white;
|
|
20
|
+
padding: 20px;
|
|
21
|
+
margin: 20px 0;
|
|
22
|
+
border-radius: 5px;
|
|
23
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
24
|
+
}
|
|
25
|
+
.component-name {
|
|
26
|
+
font-weight: bold;
|
|
27
|
+
font-size: 1.5em;
|
|
28
|
+
margin-bottom: 10px;
|
|
29
|
+
}
|
|
30
|
+
.status-healthy {
|
|
31
|
+
color: #28a745;
|
|
32
|
+
font-size: 1.2em;
|
|
33
|
+
}
|
|
34
|
+
.status-unhealthy {
|
|
35
|
+
color: #dc3545;
|
|
36
|
+
font-size: 1.2em;
|
|
37
|
+
}
|
|
38
|
+
.message {
|
|
39
|
+
color: #666;
|
|
40
|
+
font-size: 1em;
|
|
41
|
+
margin-top: 10px;
|
|
42
|
+
padding-top: 10px;
|
|
43
|
+
border-top: 1px solid #eee;
|
|
44
|
+
}
|
|
45
|
+
a {
|
|
46
|
+
color: #007bff;
|
|
47
|
+
text-decoration: none;
|
|
48
|
+
}
|
|
49
|
+
a:hover {
|
|
50
|
+
text-decoration: underline;
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
<h1><%= @component.to_s.capitalize %> Health Status</h1>
|
|
56
|
+
|
|
57
|
+
<div class="component">
|
|
58
|
+
<div class="component-name">
|
|
59
|
+
Status: <span class="status-<%= @status %>"><%= @status.to_s.upcase %></span>
|
|
60
|
+
</div>
|
|
61
|
+
<% if @result[:message] %>
|
|
62
|
+
<div class="message"><%= @result[:message] %></div>
|
|
63
|
+
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<p><a href="<%= vitals_monitor.vitals_path %>">← Back to all vitals</a></p>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
VitalsMonitor.configure do |config|
|
|
4
|
+
# Enable or disable specific components
|
|
5
|
+
# By default, all components are enabled
|
|
6
|
+
config.enable(:postgres)
|
|
7
|
+
config.enable(:redis)
|
|
8
|
+
config.enable(:sidekiq)
|
|
9
|
+
|
|
10
|
+
# Or disable specific components
|
|
11
|
+
# config.disable(:sidekiq)
|
|
12
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :vitals_monitor do
|
|
4
|
+
desc "Generate initializer file for VitalsMonitor"
|
|
5
|
+
task :install do
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
initializer_path = File.join(Dir.pwd, "config", "initializers", "vitals_monitor.rb")
|
|
9
|
+
|
|
10
|
+
# Check if we're in a Rails app
|
|
11
|
+
config_dir = File.join(Dir.pwd, "config")
|
|
12
|
+
application_rb = File.join(config_dir, "application.rb")
|
|
13
|
+
routes_rb = File.join(config_dir, "routes.rb")
|
|
14
|
+
|
|
15
|
+
unless File.directory?(config_dir) && (File.exist?(application_rb) || File.exist?(routes_rb))
|
|
16
|
+
puts "Error: This doesn't appear to be a Rails application."
|
|
17
|
+
puts "Please run this task from your Rails application root directory."
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Create initializers directory if it doesn't exist
|
|
22
|
+
FileUtils.mkdir_p(File.dirname(initializer_path))
|
|
23
|
+
|
|
24
|
+
# Check if initializer already exists
|
|
25
|
+
if File.exist?(initializer_path)
|
|
26
|
+
puts "Initializer already exists at #{initializer_path}"
|
|
27
|
+
puts "Skipping generation. Delete the file first if you want to regenerate it."
|
|
28
|
+
exit 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Generate initializer content
|
|
32
|
+
initializer_content = <<~RUBY
|
|
33
|
+
# frozen_string_literal: true
|
|
34
|
+
|
|
35
|
+
VitalsMonitor.configure do |config|
|
|
36
|
+
# Enable or disable specific components
|
|
37
|
+
# By default, all components are enabled
|
|
38
|
+
config.enable(:postgres)
|
|
39
|
+
config.enable(:redis)
|
|
40
|
+
config.enable(:sidekiq)
|
|
41
|
+
|
|
42
|
+
# Or disable specific components
|
|
43
|
+
# config.disable(:sidekiq)
|
|
44
|
+
end
|
|
45
|
+
RUBY
|
|
46
|
+
|
|
47
|
+
# Write the initializer file
|
|
48
|
+
File.write(initializer_path, initializer_content)
|
|
49
|
+
|
|
50
|
+
puts "✓ Generated initializer at #{initializer_path}"
|
|
51
|
+
|
|
52
|
+
# Add mount statement to routes.rb (host app's routes, not gem's routes)
|
|
53
|
+
mount_statement = " mount VitalsMonitor::Engine => '/vitals'"
|
|
54
|
+
mount_pattern = /mount\s+VitalsMonitor::Engine/
|
|
55
|
+
|
|
56
|
+
if File.exist?(routes_rb)
|
|
57
|
+
routes_content = File.read(routes_rb)
|
|
58
|
+
|
|
59
|
+
# Skip if this is the gem's own routes file (contains VitalsMonitor::Engine.routes.draw)
|
|
60
|
+
if routes_content.include?("VitalsMonitor::Engine.routes.draw")
|
|
61
|
+
puts "⚠ Skipping routes modification (detected gem's routes file)"
|
|
62
|
+
puts " Please manually add to your Rails app's config/routes.rb:"
|
|
63
|
+
puts " #{mount_statement.strip}"
|
|
64
|
+
# Check if mount statement already exists
|
|
65
|
+
elsif routes_content.match?(mount_pattern)
|
|
66
|
+
puts "✓ Mount statement already exists in #{routes_rb}"
|
|
67
|
+
else
|
|
68
|
+
# Add mount statement after Rails.application.routes.draw do
|
|
69
|
+
if routes_content.include?("Rails.application.routes.draw do")
|
|
70
|
+
routes_content.gsub!(/(Rails\.application\.routes\.draw\s+do)/, "\\1\n#{mount_statement}")
|
|
71
|
+
File.write(routes_rb, routes_content)
|
|
72
|
+
puts "✓ Added mount statement to #{routes_rb}"
|
|
73
|
+
# Handle alternative route file formats
|
|
74
|
+
elsif routes_content.include?("routes.draw do") && !routes_content.include?("VitalsMonitor::Engine")
|
|
75
|
+
routes_content.gsub!(/(routes\.draw\s+do)/, "\\1\n#{mount_statement}")
|
|
76
|
+
File.write(routes_rb, routes_content)
|
|
77
|
+
puts "✓ Added mount statement to #{routes_rb}"
|
|
78
|
+
# If no draw block found, add it at the end
|
|
79
|
+
else
|
|
80
|
+
routes_content += "\n#{mount_statement}\n"
|
|
81
|
+
File.write(routes_rb, routes_content)
|
|
82
|
+
puts "✓ Added mount statement to #{routes_rb}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
puts "⚠ Warning: Could not find #{routes_rb}"
|
|
87
|
+
puts " Please manually add: #{mount_statement.strip}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
puts ""
|
|
91
|
+
puts "Setup complete! Customize the configuration in #{initializer_path} if needed."
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module VitalsMonitor
|
|
6
|
+
module Checks
|
|
7
|
+
class Base
|
|
8
|
+
TIMEOUT = 5 # seconds
|
|
9
|
+
|
|
10
|
+
def check
|
|
11
|
+
raise NotImplementedError, "Subclasses must implement #check"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
def healthy(message = nil)
|
|
17
|
+
{ status: :healthy, message: message }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def unhealthy(message)
|
|
21
|
+
{ status: :unhealthy, message: message }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def timeout
|
|
25
|
+
Timeout.timeout(TIMEOUT) do
|
|
26
|
+
yield
|
|
27
|
+
end
|
|
28
|
+
rescue Timeout::Error
|
|
29
|
+
unhealthy("Health check timed out after #{TIMEOUT} seconds")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VitalsMonitor
|
|
4
|
+
module Checks
|
|
5
|
+
class Postgres < Base
|
|
6
|
+
def check
|
|
7
|
+
timeout do
|
|
8
|
+
connection = ActiveRecord::Base.connection
|
|
9
|
+
connection.execute("SELECT 1")
|
|
10
|
+
healthy
|
|
11
|
+
end
|
|
12
|
+
rescue StandardError => e
|
|
13
|
+
unhealthy("PostgreSQL connection failed: #{e.message}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VitalsMonitor
|
|
4
|
+
module Checks
|
|
5
|
+
class Redis < Base
|
|
6
|
+
def check
|
|
7
|
+
timeout do
|
|
8
|
+
redis_client = redis_connection
|
|
9
|
+
redis_client.ping
|
|
10
|
+
healthy
|
|
11
|
+
end
|
|
12
|
+
rescue StandardError => e
|
|
13
|
+
unhealthy("Redis connection failed: #{e.message}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def redis_connection
|
|
19
|
+
if defined?(::Redis) && ::Redis.respond_to?(:current)
|
|
20
|
+
::Redis.current
|
|
21
|
+
elsif defined?(::Redis) && Rails.application.config.respond_to?(:redis)
|
|
22
|
+
::Redis.new(Rails.application.config.redis)
|
|
23
|
+
elsif defined?(Sidekiq) && Sidekiq.respond_to?(:redis)
|
|
24
|
+
Sidekiq.redis { |conn| conn }
|
|
25
|
+
else
|
|
26
|
+
raise "Redis connection not configured"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VitalsMonitor
|
|
4
|
+
module Checks
|
|
5
|
+
class Sidekiq < Base
|
|
6
|
+
def check
|
|
7
|
+
timeout do
|
|
8
|
+
unless defined?(::Sidekiq)
|
|
9
|
+
return unhealthy("Sidekiq is not available")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
stats = ::Sidekiq::Stats.new
|
|
13
|
+
|
|
14
|
+
# Check if Sidekiq can connect to Redis
|
|
15
|
+
stats.processed
|
|
16
|
+
|
|
17
|
+
healthy("Processed: #{stats.processed}, Failed: #{stats.failed}, Enqueued: #{stats.enqueued}")
|
|
18
|
+
end
|
|
19
|
+
rescue StandardError => e
|
|
20
|
+
unhealthy("Sidekiq health check failed: #{e.message}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VitalsMonitor
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :enabled_components
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@enabled_components = {
|
|
9
|
+
postgres: true,
|
|
10
|
+
redis: true,
|
|
11
|
+
sidekiq: true
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def enabled?(component)
|
|
16
|
+
@enabled_components[component.to_sym] == true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def enable(component)
|
|
20
|
+
@enabled_components[component.to_sym] = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def disable(component)
|
|
24
|
+
@enabled_components[component.to_sym] = false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
|
|
5
|
+
module VitalsMonitor
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace VitalsMonitor
|
|
8
|
+
|
|
9
|
+
config.generators do |g|
|
|
10
|
+
g.test_framework :rspec
|
|
11
|
+
g.fixture_replacement :factory_bot, dir: "spec/factories"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VitalsMonitor
|
|
4
|
+
module Formatters
|
|
5
|
+
class Json
|
|
6
|
+
def self.format_all(results, overall_status)
|
|
7
|
+
{
|
|
8
|
+
status: overall_status,
|
|
9
|
+
components: results.transform_values do |result|
|
|
10
|
+
{
|
|
11
|
+
status: result[:status],
|
|
12
|
+
message: result[:message]
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.format_single(component, result)
|
|
19
|
+
{
|
|
20
|
+
component: component.to_s,
|
|
21
|
+
status: result[:status],
|
|
22
|
+
message: result[:message]
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "vitals_monitor/version"
|
|
4
|
+
require_relative "vitals_monitor/configuration"
|
|
5
|
+
|
|
6
|
+
# Health checks
|
|
7
|
+
require_relative "vitals_monitor/checks/base"
|
|
8
|
+
require_relative "vitals_monitor/checks/postgres"
|
|
9
|
+
require_relative "vitals_monitor/checks/redis"
|
|
10
|
+
require_relative "vitals_monitor/checks/sidekiq"
|
|
11
|
+
|
|
12
|
+
# Require engine only if Rails is available
|
|
13
|
+
if defined?(Rails)
|
|
14
|
+
require_relative "vitals_monitor/engine"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module VitalsMonitor
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
|
|
20
|
+
def self.config
|
|
21
|
+
@config ||= Configuration.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.configure
|
|
25
|
+
yield config if block_given?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module VitalsMonitor
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.config: () -> Configuration
|
|
8
|
+
def self.configure: () { (Configuration) -> void } -> void
|
|
9
|
+
|
|
10
|
+
class Configuration
|
|
11
|
+
attr_accessor enabled_components: Hash[Symbol, bool]
|
|
12
|
+
|
|
13
|
+
def initialize: () -> void
|
|
14
|
+
def enabled?: (Symbol | String component) -> bool
|
|
15
|
+
def enable: (Symbol | String component) -> void
|
|
16
|
+
def disable: (Symbol | String component) -> void
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module Checks
|
|
20
|
+
class Base
|
|
21
|
+
TIMEOUT: Integer
|
|
22
|
+
|
|
23
|
+
def check: () -> { status: :healthy | :unhealthy, message: String? }
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
def healthy: (?String? message) -> { status: :healthy, message: String? }
|
|
28
|
+
def unhealthy: (String message) -> { status: :unhealthy, message: String }
|
|
29
|
+
def timeout: () { () -> untyped } -> untyped
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Postgres < Base
|
|
33
|
+
def check: () -> { status: :healthy | :unhealthy, message: String? }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Redis < Base
|
|
37
|
+
def check: () -> { status: :healthy | :unhealthy, message: String? }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class Sidekiq < Base
|
|
41
|
+
def check: () -> { status: :healthy | :unhealthy, message: String? }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class Engine < ::Rails::Engine
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class VitalsController < ActionController::Base
|
|
49
|
+
def index: () -> void
|
|
50
|
+
def show: () -> void
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/vitals_monitor/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "vitals_monitor"
|
|
7
|
+
spec.version = VitalsMonitor::VERSION
|
|
8
|
+
spec.authors = ["Tomáš Landovský"]
|
|
9
|
+
spec.email = ["landovsky@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Rails engine providing health check endpoints for Postgres, Redis, and Sidekiq"
|
|
12
|
+
spec.description = "VitalsMonitor is a Rails engine that provides /vitals endpoints to monitor the health status of Postgres, Redis, and Sidekiq. Components can be enabled/disabled via configuration, and endpoints return HTML or JSON with appropriate HTTP status codes for monitoring."
|
|
13
|
+
spec.homepage = "https://github.com/landovsky/vitals_monitor"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 2.6.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/landovsky/vitals_monitor"
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/landovsky/vitals_monitor/blob/main/CHANGELOG.md"
|
|
20
|
+
|
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
+
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
spec.require_paths = ["lib"]
|
|
29
|
+
|
|
30
|
+
# Runtime dependencies
|
|
31
|
+
spec.add_dependency "rails", ">= 5.2"
|
|
32
|
+
|
|
33
|
+
# Optional dependencies (for health checks)
|
|
34
|
+
spec.add_development_dependency "pg", "~> 1.0"
|
|
35
|
+
spec.add_development_dependency "redis", "~> 4.0"
|
|
36
|
+
spec.add_development_dependency "sidekiq", "~> 6.0"
|
|
37
|
+
|
|
38
|
+
# For more information and examples about making a new gem, check out our
|
|
39
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
40
|
+
end
|