rails-health-checker 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/CHANGELOG.md +37 -0
- data/COMMANDS.md +118 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +222 -0
- data/LICENSE +21 -0
- data/README.md +174 -0
- data/Rakefile +6 -0
- data/SECURITY.md +41 -0
- data/TESTING.md +64 -0
- data/TEST_RESULTS.md +51 -0
- data/example_usage.rb +23 -0
- data/lib/rails_health_checker/checker.rb +88 -0
- data/lib/rails_health_checker/dashboard_middleware.rb +503 -0
- data/lib/rails_health_checker/gem_analyzer.rb +39 -0
- data/lib/rails_health_checker/health_middleware.rb +53 -0
- data/lib/rails_health_checker/job_analyzer.rb +108 -0
- data/lib/rails_health_checker/railtie.rb +11 -0
- data/lib/rails_health_checker/report_generator.rb +499 -0
- data/lib/rails_health_checker/system_analyzer.rb +182 -0
- data/lib/rails_health_checker/tasks.rb +63 -0
- data/lib/rails_health_checker/version.rb +3 -0
- data/lib/rails_health_checker.rb +17 -0
- data/rails_health_checker.gemspec +33 -0
- data/simple_test.rb +52 -0
- data/test_gem.rb +100 -0
- metadata +117 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
module RailsHealthChecker
|
|
2
|
+
class ReportGenerator
|
|
3
|
+
def initialize(results)
|
|
4
|
+
@results = results
|
|
5
|
+
@timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
|
6
|
+
@gem_details = fetch_gem_changelogs
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def generate_markdown
|
|
10
|
+
<<~MARKDOWN
|
|
11
|
+
# Rails Health Check Report
|
|
12
|
+
|
|
13
|
+
**Generated:** #{@timestamp}
|
|
14
|
+
**Project:** #{project_name}
|
|
15
|
+
|
|
16
|
+
## 📊 Summary
|
|
17
|
+
|
|
18
|
+
| Component | Status | Details |
|
|
19
|
+
|-----------|--------|---------|
|
|
20
|
+
| Rails | #{status_emoji(@results[:rails_version][:status])} #{@results[:rails_version][:status]} | v#{@results[:rails_version][:current]} |
|
|
21
|
+
| Ruby | #{status_emoji(@results[:ruby_version][:status])} #{@results[:ruby_version][:status]} | v#{@results[:ruby_version][:current]} |
|
|
22
|
+
| Database | #{status_emoji(@results[:database][:status])} #{@results[:database][:status]} | #{@results[:database][:connected] ? 'Connected' : 'Disconnected'} |
|
|
23
|
+
| Gems | #{gem_status_emoji} #{gem_status_text} | #{@results[:gems][:total]} total, #{@results[:gems][:outdated]} outdated |
|
|
24
|
+
| Security | #{status_emoji(@results[:security][:status])} #{@results[:security][:status]} | #{@results[:security][:outdated_count]} outdated gems |
|
|
25
|
+
| Background Jobs | #{status_emoji(@results[:jobs][:status])} #{@results[:jobs][:status]} | #{job_summary_text} |
|
|
26
|
+
|
|
27
|
+
## 🔧 Rails Environment
|
|
28
|
+
|
|
29
|
+
- **Rails Version:** #{@results[:rails_version][:current]}
|
|
30
|
+
- **Ruby Version:** #{@results[:ruby_version][:current]}
|
|
31
|
+
- **Environment:** #{Rails.env rescue 'Unknown'}
|
|
32
|
+
- **Cache Store:** #{@results[:system][:server_requirements][:cache_store][:type].split('::').last}
|
|
33
|
+
- #{@results[:system][:server_requirements][:cache_store][:explanation]}
|
|
34
|
+
- 💡 **Recommendation:** #{@results[:system][:server_requirements][:cache_store][:recommendation]}
|
|
35
|
+
|
|
36
|
+
## 💾 Database
|
|
37
|
+
|
|
38
|
+
- **Status:** #{@results[:database][:status]}
|
|
39
|
+
- **Connected:** #{@results[:database][:connected] ? '✅ Yes' : '❌ No'}
|
|
40
|
+
#{database_error_section}
|
|
41
|
+
|
|
42
|
+
## 📦 Gem Dependencies
|
|
43
|
+
|
|
44
|
+
- **Total Gems:** #{@results[:gems][:total]}
|
|
45
|
+
- **Outdated Gems:** #{@results[:gems][:outdated]}
|
|
46
|
+
- **Vulnerable Gems:** #{@results[:gems][:vulnerable]}
|
|
47
|
+
|
|
48
|
+
#{outdated_gems_section}
|
|
49
|
+
|
|
50
|
+
## 🔒 Security Analysis
|
|
51
|
+
|
|
52
|
+
- **Status:** #{@results[:security][:status]}
|
|
53
|
+
- **Outdated Packages:** #{@results[:security][:outdated_count]}
|
|
54
|
+
|
|
55
|
+
#{security_recommendations}
|
|
56
|
+
|
|
57
|
+
## ⚙️ Background Jobs
|
|
58
|
+
|
|
59
|
+
#{background_jobs_section}
|
|
60
|
+
|
|
61
|
+
## 📋 Detailed Gem Analysis
|
|
62
|
+
|
|
63
|
+
#{detailed_gem_analysis}
|
|
64
|
+
|
|
65
|
+
## 📝 Changelog & Benefits
|
|
66
|
+
|
|
67
|
+
#{changelog_section}
|
|
68
|
+
|
|
69
|
+
## ⚡ Performance Impact
|
|
70
|
+
|
|
71
|
+
#{performance_analysis}
|
|
72
|
+
|
|
73
|
+
## 🎯 Priority Actions
|
|
74
|
+
|
|
75
|
+
#{priority_actions}
|
|
76
|
+
|
|
77
|
+
## 📈 Recommendations
|
|
78
|
+
|
|
79
|
+
#{generate_recommendations}
|
|
80
|
+
|
|
81
|
+
## 📊 Health Score
|
|
82
|
+
|
|
83
|
+
#{calculate_health_score}
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
*Report generated by RailsHealthChecker v#{RailsHealthChecker::VERSION}*
|
|
87
|
+
MARKDOWN
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def save_to_file(filename = nil)
|
|
91
|
+
filename ||= "rails_health_report.md"
|
|
92
|
+
File.write(filename, generate_markdown)
|
|
93
|
+
filename
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def project_name
|
|
99
|
+
File.basename(Dir.pwd)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def status_emoji(status)
|
|
103
|
+
case status.to_s
|
|
104
|
+
when 'healthy', 'secure' then '✅'
|
|
105
|
+
when 'outdated', 'needs_attention' then '⚠️'
|
|
106
|
+
when 'unhealthy' then '❌'
|
|
107
|
+
else '❓'
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def gem_status_emoji
|
|
112
|
+
@results[:gems][:outdated] > 0 ? '⚠️' : '✅'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def gem_status_text
|
|
116
|
+
@results[:gems][:outdated] > 0 ? 'needs_attention' : 'healthy'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def database_error_section
|
|
120
|
+
return '' unless @results[:database][:error]
|
|
121
|
+
"\n- **Error:** #{@results[:database][:error]}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def outdated_gems_section
|
|
125
|
+
return '' if @results[:gems][:outdated] == 0
|
|
126
|
+
|
|
127
|
+
"\n### Outdated Gems Details\n\n" +
|
|
128
|
+
@results[:gems][:details].select { |gem| gem[:outdated] }
|
|
129
|
+
.map { |gem| "- **#{gem[:name]}** (#{gem[:version]})" }
|
|
130
|
+
.join("\n")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def security_recommendations
|
|
134
|
+
if @results[:security][:outdated_count] > 0
|
|
135
|
+
"\n⚠️ **Action Required:** Update outdated gems to latest versions"
|
|
136
|
+
else
|
|
137
|
+
"\n✅ **Good:** All gems are up to date"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def generate_recommendations
|
|
142
|
+
recommendations = []
|
|
143
|
+
|
|
144
|
+
recommendations << "- Update Rails to latest version" if @results[:rails_version][:status] == 'outdated'
|
|
145
|
+
recommendations << "- Update Ruby to latest version" if @results[:ruby_version][:status] == 'outdated'
|
|
146
|
+
recommendations << "- Fix database connection issues" if @results[:database][:status] == 'unhealthy'
|
|
147
|
+
recommendations << "- Run `bundle update` to update outdated gems" if @results[:gems][:outdated] > 0
|
|
148
|
+
recommendations << "- Review and update security-vulnerable packages" if @results[:security][:outdated_count] > 0
|
|
149
|
+
|
|
150
|
+
recommendations.empty? ? "✅ **Excellent!** Your Rails application is healthy." : recommendations.join("\n")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def fetch_gem_changelogs
|
|
154
|
+
outdated_gems = @results[:gems][:details].select { |gem| gem[:outdated] }
|
|
155
|
+
outdated_gems.map do |gem|
|
|
156
|
+
{
|
|
157
|
+
name: gem[:name],
|
|
158
|
+
current_version: gem[:version],
|
|
159
|
+
benefits: get_update_benefits(gem[:name]),
|
|
160
|
+
changelog_url: get_changelog_url(gem[:name])
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def get_update_benefits(gem_name)
|
|
166
|
+
benefits = {
|
|
167
|
+
'rails' => ['Security patches', 'Performance improvements', 'New features', 'Bug fixes'],
|
|
168
|
+
'pg' => ['Database performance', 'Connection stability', 'Security updates'],
|
|
169
|
+
'puma' => ['Server performance', 'Memory usage optimization', 'Security fixes'],
|
|
170
|
+
'bootsnap' => ['Faster boot times', 'Reduced memory usage'],
|
|
171
|
+
'turbo-rails' => ['Better SPA experience', 'Performance improvements'],
|
|
172
|
+
'stimulus-rails' => ['Enhanced JavaScript framework', 'Better DOM manipulation']
|
|
173
|
+
}
|
|
174
|
+
benefits[gem_name] || ['Security updates', 'Bug fixes', 'Performance improvements']
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def get_changelog_url(gem_name)
|
|
178
|
+
"https://github.com/search?q=#{gem_name}+changelog&type=repositories"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def detailed_gem_analysis
|
|
182
|
+
return "All gems are up to date! ✅" if @results[:gems][:outdated] == 0
|
|
183
|
+
|
|
184
|
+
analysis = []
|
|
185
|
+
critical_gems = ['rails', 'pg', 'puma', 'bootsnap']
|
|
186
|
+
|
|
187
|
+
@results[:gems][:details].select { |gem| gem[:outdated] }.each do |gem|
|
|
188
|
+
priority = critical_gems.include?(gem[:name]) ? '🔴 HIGH' : '🟡 MEDIUM'
|
|
189
|
+
analysis << "### #{gem[:name]} #{priority}"
|
|
190
|
+
analysis << "- **Current Version:** #{gem[:version]}"
|
|
191
|
+
analysis << "- **Status:** Outdated"
|
|
192
|
+
analysis << "- **Impact:** #{get_gem_impact(gem[:name])}"
|
|
193
|
+
analysis << ""
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
analysis.join("\n")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def get_gem_impact(gem_name)
|
|
200
|
+
impacts = {
|
|
201
|
+
'rails' => 'Core framework - affects entire application',
|
|
202
|
+
'pg' => 'Database connectivity and performance',
|
|
203
|
+
'puma' => 'Web server performance and stability',
|
|
204
|
+
'bootsnap' => 'Application boot time and caching',
|
|
205
|
+
'turbo-rails' => 'Frontend performance and user experience',
|
|
206
|
+
'stimulus-rails' => 'JavaScript functionality'
|
|
207
|
+
}
|
|
208
|
+
impacts[gem_name] || 'General application functionality'
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def changelog_section
|
|
212
|
+
return "No outdated gems found. ✅" if @gem_details.empty?
|
|
213
|
+
|
|
214
|
+
changelog = []
|
|
215
|
+
@gem_details.each do |gem|
|
|
216
|
+
changelog << "### #{gem[:name]}"
|
|
217
|
+
changelog << "**Benefits of updating:**"
|
|
218
|
+
gem[:benefits].each { |benefit| changelog << "- #{benefit}" }
|
|
219
|
+
changelog << "**Changelog:** [View on GitHub](#{gem[:changelog_url]})"
|
|
220
|
+
changelog << ""
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
changelog.join("\n")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def performance_analysis
|
|
227
|
+
score = calculate_performance_score
|
|
228
|
+
|
|
229
|
+
analysis = []
|
|
230
|
+
analysis << "**Overall Performance Score:** #{score}/100"
|
|
231
|
+
analysis << ""
|
|
232
|
+
|
|
233
|
+
if @results[:rails_version][:status] == 'outdated'
|
|
234
|
+
analysis << "- ⚠️ **Rails Version Impact:** Outdated Rails may have performance penalties"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
if @results[:gems][:outdated] > 10
|
|
238
|
+
analysis << "- ⚠️ **Gem Dependencies:** High number of outdated gems may impact performance"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
if @results[:database][:status] == 'unhealthy'
|
|
242
|
+
analysis << "- ❌ **Database Performance:** Connection issues will severely impact performance"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
analysis << "- ✅ **Optimization Tip:** Keep gems updated for best performance" if @results[:gems][:outdated] > 0
|
|
246
|
+
|
|
247
|
+
analysis.join("\n")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def calculate_performance_score
|
|
251
|
+
score = 100
|
|
252
|
+
score -= 20 if @results[:rails_version][:status] == 'outdated'
|
|
253
|
+
score -= 15 if @results[:ruby_version][:status] == 'outdated'
|
|
254
|
+
score -= 30 if @results[:database][:status] == 'unhealthy'
|
|
255
|
+
score -= (@results[:gems][:outdated] * 2) # 2 points per outdated gem
|
|
256
|
+
score -= (@results[:security][:outdated_count] * 3) # 3 points per security issue
|
|
257
|
+
[score, 0].max
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def priority_actions
|
|
261
|
+
actions = []
|
|
262
|
+
|
|
263
|
+
if @results[:database][:status] == 'unhealthy'
|
|
264
|
+
actions << "🔴 **CRITICAL:** Fix database connection immediately"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
if @results[:rails_version][:status] == 'outdated'
|
|
268
|
+
actions << "🟠 **HIGH:** Update Rails framework"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
if @results[:security][:outdated_count] > 5
|
|
272
|
+
actions << "🟠 **HIGH:** Address security vulnerabilities"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if @results[:gems][:outdated] > 10
|
|
276
|
+
actions << "🟡 **MEDIUM:** Update outdated gems"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
if @results[:ruby_version][:status] == 'outdated'
|
|
280
|
+
actions << "🟡 **MEDIUM:** Consider Ruby version upgrade"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
actions.empty? ? "✅ No critical actions required!" : actions.join("\n")
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def calculate_health_score
|
|
287
|
+
score = calculate_performance_score
|
|
288
|
+
reasons = get_score_reasons
|
|
289
|
+
|
|
290
|
+
status = case score
|
|
291
|
+
when 90..100 then "✅ Excellent"
|
|
292
|
+
when 70..89 then "🟡 Good"
|
|
293
|
+
when 50..69 then "🟠 Needs Attention"
|
|
294
|
+
else "🔴 Critical"
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
result = "🏆 **Overall Health Score: #{score}/100 (#{status})**\n\n"
|
|
298
|
+
|
|
299
|
+
if score < 70
|
|
300
|
+
result += "**⚠️ Critical Issues Detected:**\n#{reasons[:critical].join("\n")}\n\n" unless reasons[:critical].empty?
|
|
301
|
+
result += "**🟡 Warning Issues:**\n#{reasons[:warnings].join("\n")}\n\n" unless reasons[:warnings].empty?
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
result += "**Score Breakdown:**\n" +
|
|
305
|
+
"- Rails Version: #{@results[:rails_version][:status] == 'healthy' ? '+20' : '-20'} #{rails_score_reason}\n" +
|
|
306
|
+
"- Ruby Version: #{@results[:ruby_version][:status] == 'healthy' ? '+15' : '-15'} #{ruby_score_reason}\n" +
|
|
307
|
+
"- Database: #{@results[:database][:status] == 'healthy' ? '+30' : '-30'} #{database_score_reason}\n" +
|
|
308
|
+
"- Gem Dependencies: -#{@results[:gems][:outdated] * 2} (#{@results[:gems][:outdated]} outdated gems)\n" +
|
|
309
|
+
"- Security: -#{@results[:security][:outdated_count] * 3} (#{@results[:security][:outdated_count]} security issues)\n" +
|
|
310
|
+
"- Background Jobs: #{job_score_impact} #{job_score_reason}\n\n" +
|
|
311
|
+
"**Improvement Suggestions:**\n#{get_improvement_suggestions.join("\n")}"
|
|
312
|
+
|
|
313
|
+
result
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def job_summary_text
|
|
317
|
+
jobs = @results[:jobs]
|
|
318
|
+
return 'Not configured' if jobs[:status] == 'not_configured'
|
|
319
|
+
return 'Error' if jobs[:status] == 'error'
|
|
320
|
+
|
|
321
|
+
parts = []
|
|
322
|
+
parts << "Sidekiq: #{jobs[:sidekiq][:status]}" if jobs[:sidekiq][:available]
|
|
323
|
+
parts << "Resque: #{jobs[:resque][:status]}" if jobs[:resque][:available]
|
|
324
|
+
parts << "ActiveJob: #{jobs[:active_job][:status]}" if jobs[:active_job][:available]
|
|
325
|
+
|
|
326
|
+
parts.empty? ? 'Not configured' : parts.join(', ')
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def background_jobs_section
|
|
330
|
+
jobs = @results[:jobs]
|
|
331
|
+
return "No background job systems detected." if jobs[:status] == 'not_configured'
|
|
332
|
+
|
|
333
|
+
sections = []
|
|
334
|
+
|
|
335
|
+
if jobs[:sidekiq][:available]
|
|
336
|
+
sections << sidekiq_section(jobs[:sidekiq])
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
if jobs[:resque][:available]
|
|
340
|
+
sections << resque_section(jobs[:resque])
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
if jobs[:active_job][:available]
|
|
344
|
+
sections << active_job_section(jobs[:active_job])
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
sections.join("\n\n")
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def sidekiq_section(sidekiq)
|
|
351
|
+
return "### Sidekiq\n- **Status:** Error (#{sidekiq[:error]})" if sidekiq[:error]
|
|
352
|
+
|
|
353
|
+
<<~SECTION
|
|
354
|
+
### Sidekiq
|
|
355
|
+
- **Status:** #{sidekiq[:status].capitalize}
|
|
356
|
+
- **Processed Jobs:** #{sidekiq[:processed]}
|
|
357
|
+
- **Failed Jobs:** #{sidekiq[:failed]}
|
|
358
|
+
- **Enqueued Jobs:** #{sidekiq[:enqueued]}
|
|
359
|
+
- **Retry Queue:** #{sidekiq[:retry_size]}
|
|
360
|
+
- **Dead Queue:** #{sidekiq[:dead_size]}
|
|
361
|
+
- **Active Workers:** #{sidekiq[:workers]}
|
|
362
|
+
- **Queues:** #{sidekiq[:queues].size}
|
|
363
|
+
SECTION
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def resque_section(resque)
|
|
367
|
+
return "### Resque\n- **Status:** Error (#{resque[:error]})" if resque[:error]
|
|
368
|
+
|
|
369
|
+
<<~SECTION
|
|
370
|
+
### Resque
|
|
371
|
+
- **Status:** #{resque[:status].capitalize}
|
|
372
|
+
- **Pending Jobs:** #{resque[:pending]}
|
|
373
|
+
- **Processed Jobs:** #{resque[:processed]}
|
|
374
|
+
- **Failed Jobs:** #{resque[:failed]}
|
|
375
|
+
- **Workers:** #{resque[:workers]}
|
|
376
|
+
- **Working:** #{resque[:working]}
|
|
377
|
+
- **Queues:** #{resque[:queues]}
|
|
378
|
+
SECTION
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def active_job_section(active_job)
|
|
382
|
+
return "### ActiveJob\n- **Status:** Error (#{active_job[:error]})" if active_job[:error]
|
|
383
|
+
|
|
384
|
+
<<~SECTION
|
|
385
|
+
### ActiveJob
|
|
386
|
+
- **Status:** #{active_job[:status].capitalize}
|
|
387
|
+
- **Adapter:** #{active_job[:adapter]}
|
|
388
|
+
SECTION
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def job_score_impact
|
|
392
|
+
case @results[:jobs][:status]
|
|
393
|
+
when 'critical' then '-15'
|
|
394
|
+
when 'warning' then '-10'
|
|
395
|
+
when 'error' then '-20'
|
|
396
|
+
when 'not_configured' then '+0'
|
|
397
|
+
else '+5'
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def get_score_reasons
|
|
402
|
+
critical = []
|
|
403
|
+
warnings = []
|
|
404
|
+
|
|
405
|
+
# Critical issues (major score impact)
|
|
406
|
+
if @results[:database][:status] == 'unhealthy'
|
|
407
|
+
critical << "❌ **Database Connection Failed** - Application cannot function without database access"
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
if @results[:jobs][:status] == 'critical'
|
|
411
|
+
critical << "❌ **Background Jobs System Critical** - Job processing is severely impacted"
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
if @results[:rails_version][:status] == 'outdated'
|
|
415
|
+
critical << "⚠️ **Outdated Rails Version** - Security and performance risks"
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Warning issues (moderate score impact)
|
|
419
|
+
if @results[:gems][:outdated] > 20
|
|
420
|
+
warnings << "🟡 **Many Outdated Gems** - #{@results[:gems][:outdated]} gems need updates"
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
if @results[:security][:outdated_count] > 10
|
|
424
|
+
warnings << "🟡 **Security Vulnerabilities** - #{@results[:security][:outdated_count]} potential security issues"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
if @results[:ruby_version][:status] == 'outdated'
|
|
428
|
+
warnings << "🟡 **Outdated Ruby Version** - Consider upgrading for better performance"
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
{ critical: critical, warnings: warnings }
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def rails_score_reason
|
|
435
|
+
case @results[:rails_version][:status]
|
|
436
|
+
when 'healthy' then '(Current and supported)'
|
|
437
|
+
when 'outdated' then '(Outdated - security risk)'
|
|
438
|
+
else '(Unknown status)'
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def ruby_score_reason
|
|
443
|
+
case @results[:ruby_version][:status]
|
|
444
|
+
when 'healthy' then '(Current and supported)'
|
|
445
|
+
when 'outdated' then '(Outdated - performance impact)'
|
|
446
|
+
else '(Unknown status)'
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def database_score_reason
|
|
451
|
+
case @results[:database][:status]
|
|
452
|
+
when 'healthy' then '(Connected and responsive)'
|
|
453
|
+
when 'unhealthy' then '(Connection failed - critical issue)'
|
|
454
|
+
else '(Unknown status)'
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def job_score_reason
|
|
459
|
+
case @results[:jobs][:status]
|
|
460
|
+
when 'healthy' then '(All job systems operational)'
|
|
461
|
+
when 'warning' then '(High queue sizes detected)'
|
|
462
|
+
when 'critical' then '(Job system failures detected)'
|
|
463
|
+
when 'error' then '(Job system errors)'
|
|
464
|
+
when 'not_configured' then '(No background job system)'
|
|
465
|
+
else '(Unknown status)'
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def get_improvement_suggestions
|
|
470
|
+
suggestions = []
|
|
471
|
+
|
|
472
|
+
if @results[:database][:status] == 'unhealthy'
|
|
473
|
+
suggestions << "1. 🔴 **URGENT**: Fix database connection - check credentials, network, and database server status"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
if @results[:rails_version][:status] == 'outdated'
|
|
477
|
+
suggestions << "2. 🟠 **HIGH**: Update Rails to latest stable version for security patches"
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
if @results[:gems][:outdated] > 10
|
|
481
|
+
suggestions << "3. 🟡 **MEDIUM**: Run 'bundle update' to update outdated gems"
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
if @results[:security][:outdated_count] > 5
|
|
485
|
+
suggestions << "4. 🟡 **MEDIUM**: Review and update gems with security vulnerabilities"
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
if @results[:jobs][:status] == 'critical'
|
|
489
|
+
suggestions << "5. 🔴 **URGENT**: Check background job system configuration and restart workers"
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
if suggestions.empty?
|
|
493
|
+
suggestions << "✅ **Great!** Your application is in excellent health. Keep monitoring regularly."
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
suggestions
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
module RailsHealthChecker
|
|
2
|
+
class SystemAnalyzer
|
|
3
|
+
def analyze
|
|
4
|
+
{
|
|
5
|
+
rails_info: rails_system_info,
|
|
6
|
+
required_libs: check_required_libraries,
|
|
7
|
+
optional_libs: check_optional_libraries,
|
|
8
|
+
server_requirements: check_server_requirements,
|
|
9
|
+
cable_info: check_action_cable,
|
|
10
|
+
environment_info: environment_details
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def rails_system_info
|
|
17
|
+
{
|
|
18
|
+
version: Rails.version,
|
|
19
|
+
environment: Rails.env,
|
|
20
|
+
root: Rails.root.to_s,
|
|
21
|
+
config_loaded: Rails.application.config.loaded?,
|
|
22
|
+
eager_load: Rails.application.config.eager_load,
|
|
23
|
+
cache_classes: Rails.application.config.cache_classes
|
|
24
|
+
}
|
|
25
|
+
rescue => e
|
|
26
|
+
{ error: e.message }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def check_required_libraries
|
|
30
|
+
required = {
|
|
31
|
+
'bundler' => { required: true, available: defined?(Bundler), purpose: 'Dependency management' },
|
|
32
|
+
'rack' => { required: true, available: defined?(Rack), purpose: 'Web server interface' },
|
|
33
|
+
'activerecord' => { required: true, available: defined?(ActiveRecord), purpose: 'Database ORM' },
|
|
34
|
+
'actionpack' => { required: true, available: defined?(ActionPack), purpose: 'Web framework core' },
|
|
35
|
+
'activesupport' => { required: true, available: defined?(ActiveSupport), purpose: 'Core extensions' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
required.each do |name, info|
|
|
39
|
+
info[:status] = info[:available] ? 'loaded' : 'missing'
|
|
40
|
+
info[:version] = get_gem_version(name) if info[:available]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
required
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check_optional_libraries
|
|
47
|
+
optional = {
|
|
48
|
+
'sidekiq' => { available: defined?(Sidekiq), purpose: 'Background job processing' },
|
|
49
|
+
'resque' => { available: defined?(Resque), purpose: 'Background job processing' },
|
|
50
|
+
'redis' => { available: defined?(Redis), purpose: 'In-memory data store' },
|
|
51
|
+
'puma' => { available: defined?(Puma), purpose: 'Web server' },
|
|
52
|
+
'unicorn' => { available: defined?(Unicorn), purpose: 'Web server' },
|
|
53
|
+
'passenger' => { available: defined?(PhusionPassenger), purpose: 'Web server' },
|
|
54
|
+
'devise' => { available: defined?(Devise), purpose: 'Authentication' },
|
|
55
|
+
'cancancan' => { available: defined?(CanCan), purpose: 'Authorization' },
|
|
56
|
+
'turbo-rails' => { available: defined?(Turbo), purpose: 'SPA-like experience' },
|
|
57
|
+
'stimulus-rails' => { available: defined?(Stimulus), purpose: 'JavaScript framework' },
|
|
58
|
+
'bootsnap' => { available: defined?(Bootsnap), purpose: 'Boot optimization' },
|
|
59
|
+
'sprockets' => { available: defined?(Sprockets), purpose: 'Asset pipeline' }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
optional.each do |name, info|
|
|
63
|
+
info[:status] = info[:available] ? 'loaded' : 'not_loaded'
|
|
64
|
+
info[:version] = get_gem_version(name) if info[:available]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
optional
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check_server_requirements
|
|
71
|
+
{
|
|
72
|
+
web_server: detect_web_server,
|
|
73
|
+
database: detect_database_adapter,
|
|
74
|
+
cache_store: detect_cache_store,
|
|
75
|
+
session_store: detect_session_store,
|
|
76
|
+
asset_host: (Rails.application.config.asset_host rescue nil)
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def check_action_cable
|
|
81
|
+
return { available: false } unless defined?(ActionCable)
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
available: true,
|
|
85
|
+
adapter: (ActionCable.server.config.cable[:adapter] rescue 'unknown'),
|
|
86
|
+
url: (ActionCable.server.config.cable[:url] rescue nil),
|
|
87
|
+
allowed_request_origins: (ActionCable.server.config.allowed_request_origins rescue []),
|
|
88
|
+
status: action_cable_status
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def environment_details
|
|
93
|
+
{
|
|
94
|
+
ruby_version: RUBY_VERSION,
|
|
95
|
+
ruby_platform: RUBY_PLATFORM,
|
|
96
|
+
rails_env: Rails.env,
|
|
97
|
+
rack_env: ENV['RACK_ENV'],
|
|
98
|
+
database_url: ENV['DATABASE_URL'] ? 'configured' : 'not_set',
|
|
99
|
+
redis_url: ENV['REDIS_URL'] ? 'configured' : 'not_set',
|
|
100
|
+
secret_key_base: ENV['SECRET_KEY_BASE'] ? 'configured' : 'not_set'
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def get_gem_version(gem_name)
|
|
105
|
+
Gem.loaded_specs[gem_name]&.version&.to_s || 'unknown'
|
|
106
|
+
rescue
|
|
107
|
+
'unknown'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def detect_web_server
|
|
111
|
+
return 'puma' if defined?(Puma)
|
|
112
|
+
return 'unicorn' if defined?(Unicorn)
|
|
113
|
+
return 'passenger' if defined?(PhusionPassenger)
|
|
114
|
+
return 'thin' if defined?(Thin)
|
|
115
|
+
return 'webrick' if defined?(WEBrick)
|
|
116
|
+
'unknown'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def detect_database_adapter
|
|
120
|
+
ActiveRecord::Base.connection.adapter_name
|
|
121
|
+
rescue
|
|
122
|
+
'unknown'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def detect_cache_store
|
|
126
|
+
cache_class = Rails.cache.class.name
|
|
127
|
+
{
|
|
128
|
+
type: cache_class,
|
|
129
|
+
explanation: get_cache_explanation(cache_class),
|
|
130
|
+
recommendation: get_cache_recommendation(cache_class)
|
|
131
|
+
}
|
|
132
|
+
rescue
|
|
133
|
+
{
|
|
134
|
+
type: 'unknown',
|
|
135
|
+
explanation: 'Unable to detect cache store',
|
|
136
|
+
recommendation: 'Check Rails cache configuration'
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def detect_session_store
|
|
141
|
+
Rails.application.config.session_store.name
|
|
142
|
+
rescue
|
|
143
|
+
'unknown'
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def action_cable_status
|
|
147
|
+
return 'healthy' if ActionCable.server.config.cable[:adapter] != 'test'
|
|
148
|
+
'not_configured'
|
|
149
|
+
rescue
|
|
150
|
+
'error'
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def get_cache_explanation(cache_class)
|
|
154
|
+
explanations = {
|
|
155
|
+
'ActiveSupport::Cache::NullStore' => 'No caching (common in development)',
|
|
156
|
+
'ActiveSupport::Cache::MemoryStore' => 'In-memory caching (single process)',
|
|
157
|
+
'ActiveSupport::Cache::FileStore' => 'File-based caching (disk storage)',
|
|
158
|
+
'ActiveSupport::Cache::RedisStore' => 'Redis-based caching (recommended for production)',
|
|
159
|
+
'ActiveSupport::Cache::MemCacheStore' => 'Memcached-based caching (production ready)',
|
|
160
|
+
'ActiveSupport::Cache::RedisCacheStore' => 'Redis caching with Rails 5.2+ features'
|
|
161
|
+
}
|
|
162
|
+
explanations[cache_class] || 'Custom or unknown cache store'
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def get_cache_recommendation(cache_class)
|
|
166
|
+
case cache_class
|
|
167
|
+
when 'ActiveSupport::Cache::NullStore'
|
|
168
|
+
Rails.env.production? ? 'Configure Redis or Memcached for production' : 'OK for development'
|
|
169
|
+
when 'ActiveSupport::Cache::MemoryStore'
|
|
170
|
+
'Consider Redis/Memcached for multi-server deployments'
|
|
171
|
+
when 'ActiveSupport::Cache::FileStore'
|
|
172
|
+
'Consider Redis/Memcached for better performance'
|
|
173
|
+
when 'ActiveSupport::Cache::RedisStore', 'ActiveSupport::Cache::RedisCacheStore'
|
|
174
|
+
'Excellent choice for production'
|
|
175
|
+
when 'ActiveSupport::Cache::MemCacheStore'
|
|
176
|
+
'Good choice for production'
|
|
177
|
+
else
|
|
178
|
+
'Verify cache store configuration'
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|