solidstats 2.0.0 → 3.0.0.beta.1
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 +4 -4
- data/CHANGELOG.md +12 -63
- data/README.md +27 -33
- data/Rakefile +3 -3
- data/app/assets/stylesheets/solidstats/application.css +1 -6
- data/app/assets/stylesheets/solidstats/dashboard.css +48 -0
- data/app/controllers/solidstats/dashboard_controller.rb +81 -62
- data/app/controllers/solidstats/logs_controller.rb +72 -0
- data/app/controllers/solidstats/performance_controller.rb +25 -0
- data/app/controllers/solidstats/productivity_controller.rb +39 -0
- data/app/controllers/solidstats/quality_controller.rb +152 -0
- data/app/controllers/solidstats/securities_controller.rb +30 -0
- data/app/helpers/solidstats/application_helper.rb +124 -11
- data/app/helpers/solidstats/performance_helper.rb +87 -0
- data/app/helpers/solidstats/productivity_helper.rb +38 -0
- data/app/services/solidstats/bundler_audit_service.rb +206 -0
- data/app/services/solidstats/coverage_compass_service.rb +335 -0
- data/app/services/solidstats/load_lens_service.rb +454 -0
- data/app/services/solidstats/log_size_monitor_service.rb +207 -76
- data/app/services/solidstats/my_todo_service.rb +242 -0
- data/app/services/solidstats/style_patrol_service.rb +319 -0
- data/app/views/layouts/solidstats/application.html.erb +8 -2
- data/app/views/layouts/solidstats/dashboard.html.erb +84 -0
- data/app/views/solidstats/dashboard/dashboard.html.erb +39 -0
- data/app/views/solidstats/logs/logs_size.html.erb +409 -0
- data/app/views/solidstats/performance/load_lens.html.erb +158 -0
- data/app/views/solidstats/productivity/_todo_list.html.erb +49 -0
- data/app/views/solidstats/productivity/my_todos.html.erb +84 -0
- data/app/views/solidstats/quality/coverage_compass.html.erb +420 -0
- data/app/views/solidstats/quality/style_patrol.html.erb +463 -0
- data/app/views/solidstats/securities/bundler_audit.html.erb +345 -0
- data/app/views/solidstats/shared/_dashboard_card.html.erb +160 -0
- data/app/views/solidstats/shared/_quick_actions.html.erb +26 -0
- data/config/routes.rb +32 -7
- data/lib/generators/solidstats/install/install_generator.rb +28 -2
- data/lib/generators/solidstats/install/templates/README +7 -0
- data/lib/solidstats/engine.rb +9 -114
- data/lib/solidstats/version.rb +1 -1
- data/lib/solidstats.rb +2 -299
- data/lib/tasks/solidstats_install.rake +2 -122
- data/lib/tasks/solidstats_performance.rake +84 -0
- metadata +32 -103
- data/app/assets/javascripts/solidstats/application.js +0 -257
- data/app/assets/javascripts/solidstats/dashboard.js +0 -225
- data/app/assets/javascripts/solidstats/gem_metadata.js +0 -554
- data/app/assets/stylesheets/solidstats/components/action_button.css +0 -99
- data/app/assets/stylesheets/solidstats/components/dashboard.css +0 -151
- data/app/assets/stylesheets/solidstats/components/dashboard_header.css +0 -93
- data/app/assets/stylesheets/solidstats/components/dashboard_layout.css +0 -97
- data/app/assets/stylesheets/solidstats/components/gem_metadata.css +0 -1403
- data/app/assets/stylesheets/solidstats/components/navigation.css +0 -80
- data/app/assets/stylesheets/solidstats/components/quick_navigation.css +0 -54
- data/app/assets/stylesheets/solidstats/components/security.css +0 -332
- data/app/assets/stylesheets/solidstats/components/status_badge.css +0 -58
- data/app/assets/stylesheets/solidstats/components/summary_card.css +0 -66
- data/app/assets/stylesheets/solidstats/components/tab_navigation.css +0 -95
- data/app/components/solidstats/base_component.rb +0 -88
- data/app/components/solidstats/code_quality/code_quality_section_component.html.erb +0 -0
- data/app/components/solidstats/code_quality/code_quality_section_component.rb +0 -0
- data/app/components/solidstats/code_quality/section_component.html.erb +0 -45
- data/app/components/solidstats/code_quality/section_component.rb +0 -34
- data/app/components/solidstats/dashboard_header_component.html.erb +0 -39
- data/app/components/solidstats/dashboard_header_component.rb +0 -33
- data/app/components/solidstats/previews/action_button_component_preview/button_vs_link.html.erb +0 -6
- data/app/components/solidstats/previews/action_button_component_preview/sizes.html.erb +0 -6
- data/app/components/solidstats/previews/action_button_component_preview/variants.html.erb +0 -6
- data/app/components/solidstats/previews/action_button_component_preview/with_icons.html.erb +0 -6
- data/app/components/solidstats/previews/action_button_component_preview.rb +0 -64
- data/app/components/solidstats/previews/navigation_component_preview.rb +0 -74
- data/app/components/solidstats/previews/stats_overview_component_preview.rb +0 -100
- data/app/components/solidstats/previews/status_badge_component_preview/sizes.html.erb +0 -6
- data/app/components/solidstats/previews/status_badge_component_preview/statuses.html.erb +0 -6
- data/app/components/solidstats/previews/status_badge_component_preview/with_icons.html.erb +0 -6
- data/app/components/solidstats/previews/status_badge_component_preview.rb +0 -49
- data/app/components/solidstats/previews/summary_card_component_preview/clickable.html.erb +0 -9
- data/app/components/solidstats/previews/summary_card_component_preview/dashboard_layout.html.erb +0 -9
- data/app/components/solidstats/previews/summary_card_component_preview/statuses.html.erb +0 -6
- data/app/components/solidstats/previews/summary_card_component_preview/value_formats.html.erb +0 -6
- data/app/components/solidstats/previews/summary_card_component_preview.rb +0 -67
- data/app/components/solidstats/quick_navigation_component.html.erb +0 -8
- data/app/components/solidstats/quick_navigation_component.rb +0 -21
- data/app/components/solidstats/security/gem_impact_analysis_component.html.erb +0 -44
- data/app/components/solidstats/security/gem_impact_analysis_component.rb +0 -45
- data/app/components/solidstats/security/overview_component.html.erb +0 -21
- data/app/components/solidstats/security/overview_component.rb +0 -104
- data/app/components/solidstats/security/section_component.html.erb +0 -26
- data/app/components/solidstats/security/section_component.rb +0 -52
- data/app/components/solidstats/security/timeline_component.html.erb +0 -39
- data/app/components/solidstats/security/timeline_component.rb +0 -43
- data/app/components/solidstats/tasks_section_component.html.erb +0 -17
- data/app/components/solidstats/tasks_section_component.rb +0 -22
- data/app/components/solidstats/ui/action_button_component.html.erb +0 -6
- data/app/components/solidstats/ui/action_button_component.rb +0 -71
- data/app/components/solidstats/ui/dashboard_layout_component.html.erb +0 -19
- data/app/components/solidstats/ui/dashboard_layout_component.rb +0 -85
- data/app/components/solidstats/ui/navigation_component.html.erb +0 -34
- data/app/components/solidstats/ui/navigation_component.rb +0 -72
- data/app/components/solidstats/ui/stats_overview_component.html.erb +0 -14
- data/app/components/solidstats/ui/stats_overview_component.rb +0 -78
- data/app/components/solidstats/ui/status_badge_component.html.erb +0 -6
- data/app/components/solidstats/ui/status_badge_component.rb +0 -42
- data/app/components/solidstats/ui/summary_card_component.html.erb +0 -12
- data/app/components/solidstats/ui/summary_card_component.rb +0 -63
- data/app/components/solidstats/ui/tab_navigation_component.html.erb +0 -22
- data/app/components/solidstats/ui/tab_navigation_component.rb +0 -79
- data/app/controllers/solidstats/gem_metadata_controller.rb +0 -12
- data/app/services/solidstats/audit_service.rb +0 -56
- data/app/services/solidstats/data_collector_service.rb +0 -83
- data/app/services/solidstats/gem_metadata/fetcher_service.rb +0 -136
- data/app/services/solidstats/todo_service.rb +0 -114
- data/app/views/solidstats/dashboard/_log_monitor.html.erb +0 -759
- data/app/views/solidstats/dashboard/_todos.html.erb +0 -151
- data/app/views/solidstats/dashboard/audit/_additional_styles.css +0 -22
- data/app/views/solidstats/dashboard/audit/_audit_badge.html.erb +0 -5
- data/app/views/solidstats/dashboard/audit/_audit_details.html.erb +0 -495
- data/app/views/solidstats/dashboard/audit/_audit_summary.html.erb +0 -26
- data/app/views/solidstats/dashboard/audit/_no_vulnerabilities.html.erb +0 -3
- data/app/views/solidstats/dashboard/audit/_security_audit.html.erb +0 -14
- data/app/views/solidstats/dashboard/audit/_vulnerabilities_table.html.erb +0 -1120
- data/app/views/solidstats/dashboard/audit/_vulnerability_details.html.erb +0 -63
- data/app/views/solidstats/dashboard/index.html.erb +0 -81
- data/app/views/solidstats/gem_metadata/_panel.html.erb +0 -419
- data/lib/generators/solidstats/feature/feature_generator.rb +0 -170
- data/lib/generators/solidstats/feature/templates/component.html.erb +0 -84
- data/lib/generators/solidstats/feature/templates/component.rb.erb +0 -103
- data/lib/generators/solidstats/feature/templates/component.scss +0 -243
- data/lib/generators/solidstats/feature/templates/component_test.rb.erb +0 -183
- data/lib/generators/solidstats/feature/templates/controller.rb.erb +0 -44
- data/lib/generators/solidstats/feature/templates/controller_test.rb.erb +0 -111
- data/lib/generators/solidstats/feature/templates/detail_view.html.erb +0 -755
- data/lib/generators/solidstats/feature/templates/preview.rb.erb +0 -107
- data/lib/generators/solidstats/feature/templates/service.rb.erb +0 -132
- data/lib/generators/solidstats/feature/templates/service_test.rb.erb +0 -109
- data/lib/generators/solidstats/install_generator.rb +0 -109
- data/lib/generators/solidstats/templates/initializer.rb +0 -112
- data/lib/solidstats/asset_compatibility.rb +0 -238
- data/lib/solidstats/asset_manifest.rb +0 -205
- data/lib/tasks/solidstats_tasks.rake +0 -4
@@ -0,0 +1,454 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solidstats
|
4
|
+
# LoadLens - Development Performance Monitoring Service
|
5
|
+
# Parses Rails development logs to extract performance metrics
|
6
|
+
class LoadLensService
|
7
|
+
DATA_DIR = Rails.root.join('solidstats')
|
8
|
+
LOG_FILE = Rails.root.join('log', 'development.log')
|
9
|
+
POSITION_FILE = DATA_DIR.join('last_position.txt')
|
10
|
+
CACHE_FILE = "loadlens.json"
|
11
|
+
SUMMARY_FILE = "summary.json"
|
12
|
+
RETENTION_DAYS = 7
|
13
|
+
|
14
|
+
# Regex patterns for parsing Rails development log
|
15
|
+
REQUEST_START_REGEX = /Started\s+(\w+)\s+"([^"]+)"\s+for\s+[\d\.:]+\s+at\s+([\d\-\s:]+)/
|
16
|
+
CONTROLLER_ACTION_REGEX = /Processing\s+by\s+([^#]+)#(\w+)\s+as/
|
17
|
+
# Updated to capture all timing info from the completion line
|
18
|
+
COMPLETED_REGEX = /Completed\s+(\d+)\s+\w+\s+in\s+([\d\.]+)ms(?:\s+\(([^)]+)\))?/
|
19
|
+
# Separate patterns for extracting times from the completion line details
|
20
|
+
VIEW_RENDERING_REGEX = /Views:\s+([\d\.]+)ms/
|
21
|
+
ACTIVERECORD_REGEX = /ActiveRecord:\s+([\d\.]+)ms/
|
22
|
+
|
23
|
+
def self.get_performance_data
|
24
|
+
cache_file_path = solidstats_cache_path(CACHE_FILE)
|
25
|
+
|
26
|
+
if File.exist?(cache_file_path) && cache_fresh?(cache_file_path, 15.minutes)
|
27
|
+
raw_data = JSON.parse(File.read(cache_file_path))
|
28
|
+
deep_indifferent_access(raw_data)
|
29
|
+
else
|
30
|
+
scan_and_cache
|
31
|
+
end
|
32
|
+
rescue JSON::ParserError, Errno::ENOENT
|
33
|
+
Rails.logger.error("Error reading performance cache, regenerating...")
|
34
|
+
scan_and_cache
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.scan_and_cache
|
38
|
+
performance_data = scan_development_log
|
39
|
+
|
40
|
+
# Cache the performance data
|
41
|
+
cache_performance_data(performance_data)
|
42
|
+
|
43
|
+
# Update summary.json with performance monitoring card
|
44
|
+
update_summary_json(performance_data)
|
45
|
+
|
46
|
+
deep_indifferent_access(performance_data)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.refresh_data
|
50
|
+
# Force refresh by removing cache and regenerating
|
51
|
+
cache_file_path = solidstats_cache_path(CACHE_FILE)
|
52
|
+
File.delete(cache_file_path) if File.exist?(cache_file_path)
|
53
|
+
scan_and_cache
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.parse_log_and_save
|
57
|
+
return unless Rails.env.development?
|
58
|
+
return unless File.exist?(LOG_FILE)
|
59
|
+
|
60
|
+
ensure_data_directory
|
61
|
+
cleanup_old_files
|
62
|
+
|
63
|
+
begin
|
64
|
+
last_position = read_last_position
|
65
|
+
processed_count = 0
|
66
|
+
current_requests = []
|
67
|
+
|
68
|
+
File.open(LOG_FILE, 'r') do |file|
|
69
|
+
file.seek(last_position)
|
70
|
+
|
71
|
+
file.each_line do |line|
|
72
|
+
process_line(line.strip, current_requests)
|
73
|
+
update_last_position(file.pos)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Process any remaining incomplete requests
|
78
|
+
current_requests.each do |req|
|
79
|
+
if req[:completed]
|
80
|
+
save_request(req)
|
81
|
+
processed_count += 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
Rails.logger.info("DevLogParser: Processed #{processed_count} requests")
|
86
|
+
{ success: true, processed: processed_count }
|
87
|
+
rescue => e
|
88
|
+
Rails.logger.error("DevLogParser: Failed to parse development log: #{e.message}")
|
89
|
+
{ success: false, error: e.message }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def self.deep_indifferent_access(data)
|
96
|
+
if data.is_a?(Hash)
|
97
|
+
data.with_indifferent_access.transform_values do |value|
|
98
|
+
deep_indifferent_access(value)
|
99
|
+
end
|
100
|
+
elsif data.is_a?(Array)
|
101
|
+
data.map { |element| deep_indifferent_access(element) }
|
102
|
+
else
|
103
|
+
data
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.scan_development_log
|
108
|
+
# Load recent performance data for dashboard
|
109
|
+
data_files = []
|
110
|
+
|
111
|
+
# Get last 7 days of data files
|
112
|
+
7.times do |i|
|
113
|
+
date = i.days.ago.to_date
|
114
|
+
file_path = DATA_DIR.join("perf_#{date.strftime('%Y-%m-%d')}.json")
|
115
|
+
|
116
|
+
if File.exist?(file_path)
|
117
|
+
file_data = JSON.parse(File.read(file_path))
|
118
|
+
data_files.concat(file_data)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Fallback: If no data files exist, parse recent entries from development.log
|
123
|
+
if data_files.empty? && File.exist?(LOG_FILE) && Rails.env.development?
|
124
|
+
Rails.logger.info("DevLogParser: No data files found, parsing recent development log entries")
|
125
|
+
data_files = parse_recent_log_entries
|
126
|
+
end
|
127
|
+
|
128
|
+
# Calculate metrics
|
129
|
+
calculate_performance_metrics(data_files)
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.parse_recent_log_entries
|
133
|
+
# Parse recent entries from development.log as fallback when no data files exist
|
134
|
+
recent_requests = []
|
135
|
+
current_requests = []
|
136
|
+
lines_processed = 0
|
137
|
+
max_lines = 2000 # Process last 2000 lines for initial bootstrap
|
138
|
+
|
139
|
+
begin
|
140
|
+
# Get the last N lines from the log file efficiently
|
141
|
+
log_lines = []
|
142
|
+
File.open(LOG_FILE, 'r') do |file|
|
143
|
+
file.each_line { |line| log_lines << line.strip }
|
144
|
+
end
|
145
|
+
|
146
|
+
# Take the last max_lines entries
|
147
|
+
log_lines = log_lines.last(max_lines) if log_lines.size > max_lines
|
148
|
+
|
149
|
+
# Process each line using existing parsing logic
|
150
|
+
log_lines.each do |line|
|
151
|
+
process_line(line, current_requests)
|
152
|
+
lines_processed += 1
|
153
|
+
|
154
|
+
# Collect completed requests
|
155
|
+
current_requests.each do |req|
|
156
|
+
if req[:completed]
|
157
|
+
# Convert to the same format as saved requests
|
158
|
+
clean_request = {
|
159
|
+
'controller' => req[:controller],
|
160
|
+
'action' => req[:action],
|
161
|
+
'http_method' => req[:http_method],
|
162
|
+
'path' => req[:path],
|
163
|
+
'status' => req[:status],
|
164
|
+
'total_time_ms' => req[:total_time_ms] || 0.0,
|
165
|
+
'view_time_ms' => req[:view_time_ms] || 0.0,
|
166
|
+
'activerecord_time_ms' => req[:activerecord_time_ms] || 0.0,
|
167
|
+
'timestamp' => req[:timestamp]
|
168
|
+
}
|
169
|
+
|
170
|
+
# Only include complete requests with controller/action
|
171
|
+
if clean_request['controller'] && clean_request['action']
|
172
|
+
recent_requests << clean_request
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Remove completed requests from processing queue
|
178
|
+
current_requests.reject! { |req| req[:completed] }
|
179
|
+
end
|
180
|
+
|
181
|
+
Rails.logger.info("DevLogParser: Parsed #{recent_requests.size} recent requests from #{lines_processed} log lines")
|
182
|
+
recent_requests
|
183
|
+
|
184
|
+
rescue => e
|
185
|
+
Rails.logger.error("DevLogParser: Failed to parse recent log entries: #{e.message}")
|
186
|
+
[]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def self.calculate_performance_metrics(requests)
|
191
|
+
return default_metrics if requests.empty?
|
192
|
+
|
193
|
+
total_requests = requests.size
|
194
|
+
avg_response_time = (requests.sum { |req| req['total_time_ms'] || 0 } / total_requests).round(2)
|
195
|
+
avg_view_time = (requests.sum { |req| req['view_time_ms'] || 0 } / total_requests).round(2)
|
196
|
+
avg_db_time = (requests.sum { |req| req['activerecord_time_ms'] || 0 } / total_requests).round(2)
|
197
|
+
slow_requests = requests.count { |req| (req['total_time_ms'] || 0) > 1000 }
|
198
|
+
error_rate = ((requests.count { |req| (req['status'] || 200) >= 400 }.to_f / total_requests) * 100).round(2)
|
199
|
+
|
200
|
+
status = determine_status(avg_response_time, error_rate, slow_requests, total_requests)
|
201
|
+
|
202
|
+
{
|
203
|
+
summary: {
|
204
|
+
total_requests: total_requests,
|
205
|
+
avg_response_time: avg_response_time,
|
206
|
+
avg_view_time: avg_view_time,
|
207
|
+
avg_db_time: avg_db_time,
|
208
|
+
slow_requests: slow_requests,
|
209
|
+
error_rate: error_rate,
|
210
|
+
status: status,
|
211
|
+
last_updated: Time.current.iso8601
|
212
|
+
},
|
213
|
+
recent_requests: requests.sort_by { |req| req['timestamp'] }.reverse.first(20),
|
214
|
+
last_updated: Time.current.iso8601
|
215
|
+
}
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.default_metrics
|
219
|
+
{
|
220
|
+
summary: {
|
221
|
+
total_requests: 0,
|
222
|
+
avg_response_time: 0,
|
223
|
+
avg_view_time: 0,
|
224
|
+
avg_db_time: 0,
|
225
|
+
slow_requests: 0,
|
226
|
+
error_rate: 0,
|
227
|
+
status: 'info',
|
228
|
+
last_updated: Time.current.iso8601
|
229
|
+
},
|
230
|
+
recent_requests: [],
|
231
|
+
last_updated: Time.current.iso8601
|
232
|
+
}
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.determine_status(avg_response_time, error_rate, slow_requests, total_requests)
|
236
|
+
return 'error' if error_rate > 10
|
237
|
+
return 'warning' if avg_response_time > 1000 || slow_requests > (total_requests * 0.1)
|
238
|
+
return 'success'
|
239
|
+
end
|
240
|
+
|
241
|
+
def self.process_line(line, current_requests)
|
242
|
+
# Start of new request
|
243
|
+
if match = line.match(REQUEST_START_REGEX)
|
244
|
+
method, path, timestamp = match.captures
|
245
|
+
current_requests << {
|
246
|
+
http_method: method,
|
247
|
+
path: path,
|
248
|
+
timestamp: parse_timestamp(timestamp),
|
249
|
+
started_at: Time.current
|
250
|
+
}
|
251
|
+
|
252
|
+
# Controller and action info
|
253
|
+
elsif match = line.match(CONTROLLER_ACTION_REGEX)
|
254
|
+
controller, action = match.captures
|
255
|
+
if current_request = current_requests.last
|
256
|
+
current_request[:controller] = controller
|
257
|
+
current_request[:action] = action
|
258
|
+
end
|
259
|
+
|
260
|
+
# Request completion with timing
|
261
|
+
elsif match = line.match(COMPLETED_REGEX)
|
262
|
+
status, total_time, timing_details = match.captures
|
263
|
+
if current_request = current_requests.last
|
264
|
+
current_request[:status] = status.to_i
|
265
|
+
current_request[:total_time_ms] = total_time.to_f
|
266
|
+
current_request[:completed] = true
|
267
|
+
|
268
|
+
# Extract view and ActiveRecord times from timing details if present
|
269
|
+
if timing_details
|
270
|
+
if view_match = timing_details.match(VIEW_RENDERING_REGEX)
|
271
|
+
current_request[:view_time_ms] = view_match[1].to_f
|
272
|
+
end
|
273
|
+
|
274
|
+
if ar_match = timing_details.match(ACTIVERECORD_REGEX)
|
275
|
+
current_request[:activerecord_time_ms] = ar_match[1].to_f
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# View rendering time
|
281
|
+
elsif match = line.match(VIEW_RENDERING_REGEX)
|
282
|
+
view_time = match.captures.first
|
283
|
+
if current_request = current_requests.last
|
284
|
+
current_request[:view_time_ms] = view_time.to_f
|
285
|
+
end
|
286
|
+
|
287
|
+
# ActiveRecord time
|
288
|
+
elsif match = line.match(ACTIVERECORD_REGEX)
|
289
|
+
ar_time = match.captures.first
|
290
|
+
if current_request = current_requests.last
|
291
|
+
current_request[:activerecord_time_ms] = ar_time.to_f
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Save and remove completed requests
|
296
|
+
current_requests.reject! do |request|
|
297
|
+
if request[:completed]
|
298
|
+
save_request(request)
|
299
|
+
true
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def self.save_request(request)
|
305
|
+
# Clean up request data
|
306
|
+
clean_request = {
|
307
|
+
controller: request[:controller],
|
308
|
+
action: request[:action],
|
309
|
+
http_method: request[:http_method],
|
310
|
+
path: request[:path],
|
311
|
+
status: request[:status],
|
312
|
+
total_time_ms: request[:total_time_ms] || 0.0,
|
313
|
+
view_time_ms: request[:view_time_ms] || 0.0,
|
314
|
+
activerecord_time_ms: request[:activerecord_time_ms] || 0.0,
|
315
|
+
timestamp: request[:timestamp]
|
316
|
+
}
|
317
|
+
|
318
|
+
# Skip incomplete requests
|
319
|
+
return unless clean_request[:controller] && clean_request[:action]
|
320
|
+
|
321
|
+
perf_file = current_perf_file
|
322
|
+
|
323
|
+
# Read existing data or create new array
|
324
|
+
existing_data = if File.exist?(perf_file)
|
325
|
+
JSON.parse(File.read(perf_file))
|
326
|
+
else
|
327
|
+
[]
|
328
|
+
end
|
329
|
+
|
330
|
+
# Add new request
|
331
|
+
existing_data << clean_request
|
332
|
+
|
333
|
+
# Write back to file
|
334
|
+
File.write(perf_file, JSON.pretty_generate(existing_data))
|
335
|
+
end
|
336
|
+
|
337
|
+
def self.current_perf_file
|
338
|
+
date_suffix = Date.current.strftime('%Y-%m-%d')
|
339
|
+
DATA_DIR.join("perf_#{date_suffix}.json")
|
340
|
+
end
|
341
|
+
|
342
|
+
def self.ensure_data_directory
|
343
|
+
DATA_DIR.mkpath unless DATA_DIR.exist?
|
344
|
+
end
|
345
|
+
|
346
|
+
def self.cleanup_old_files
|
347
|
+
cutoff_date = RETENTION_DAYS.days.ago.to_date
|
348
|
+
|
349
|
+
Dir.glob(DATA_DIR.join('perf_*.json')).each do |file|
|
350
|
+
if match = File.basename(file).match(/perf_(\d{4}-\d{2}-\d{2})\.json/)
|
351
|
+
file_date = Date.parse(match[1])
|
352
|
+
File.delete(file) if file_date < cutoff_date
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def self.read_last_position
|
358
|
+
return 0 unless File.exist?(POSITION_FILE)
|
359
|
+
File.read(POSITION_FILE).to_i
|
360
|
+
end
|
361
|
+
|
362
|
+
def self.update_last_position(position)
|
363
|
+
File.write(POSITION_FILE, position.to_s)
|
364
|
+
end
|
365
|
+
|
366
|
+
def self.parse_timestamp(timestamp_str)
|
367
|
+
# Handle Rails timestamp format: "2025-06-10 14:30:45 +0000"
|
368
|
+
Time.parse(timestamp_str).iso8601
|
369
|
+
rescue
|
370
|
+
Time.current.iso8601
|
371
|
+
end
|
372
|
+
|
373
|
+
def self.cache_performance_data(data)
|
374
|
+
cache_file_path = solidstats_cache_path(CACHE_FILE)
|
375
|
+
ensure_solidstats_directory
|
376
|
+
|
377
|
+
File.write(cache_file_path, JSON.pretty_generate(data))
|
378
|
+
rescue => e
|
379
|
+
Rails.logger.error("Failed to cache performance data: #{e.message}")
|
380
|
+
end
|
381
|
+
|
382
|
+
def self.update_summary_json(performance_data)
|
383
|
+
summary_file_path = solidstats_cache_path(SUMMARY_FILE)
|
384
|
+
|
385
|
+
# Read existing summary or create new one
|
386
|
+
begin
|
387
|
+
existing_summary = File.exist?(summary_file_path) ? JSON.parse(File.read(summary_file_path)) : {}
|
388
|
+
rescue JSON::ParserError
|
389
|
+
existing_summary = {}
|
390
|
+
end
|
391
|
+
|
392
|
+
summary = performance_data[:summary]
|
393
|
+
|
394
|
+
# Create badges based on performance metrics
|
395
|
+
badges = []
|
396
|
+
badges << { "text" => "#{summary[:total_requests]} Requests", "color" => "info" }
|
397
|
+
|
398
|
+
case summary[:status]
|
399
|
+
when 'error'
|
400
|
+
badges << { "text" => "High Errors", "color" => "error" }
|
401
|
+
when 'warning'
|
402
|
+
badges << { "text" => "Slow Responses", "color" => "warning" }
|
403
|
+
else
|
404
|
+
badges << { "text" => "Healthy", "color" => "success" }
|
405
|
+
end
|
406
|
+
|
407
|
+
if summary[:avg_response_time] > 0
|
408
|
+
badges << { "text" => "#{summary[:avg_response_time]}ms avg", "color" => "neutral" }
|
409
|
+
end
|
410
|
+
|
411
|
+
# Update the LoadLens monitoring entry
|
412
|
+
existing_summary["LoadLens"] = {
|
413
|
+
"icon" => "activity",
|
414
|
+
"status" => summary[:status],
|
415
|
+
"value" => generate_performance_message(summary),
|
416
|
+
"last_updated" => summary[:last_updated],
|
417
|
+
"url" => "/solidstats/performance/load_lens",
|
418
|
+
"badges" => badges
|
419
|
+
}
|
420
|
+
|
421
|
+
# Write updated summary
|
422
|
+
File.write(summary_file_path, JSON.pretty_generate(existing_summary))
|
423
|
+
rescue => e
|
424
|
+
Rails.logger.error("Failed to update summary.json: #{e.message}")
|
425
|
+
end
|
426
|
+
|
427
|
+
def self.generate_performance_message(summary)
|
428
|
+
if summary[:total_requests] == 0
|
429
|
+
"No requests tracked"
|
430
|
+
elsif summary[:status] == 'error'
|
431
|
+
"#{summary[:error_rate]}% error rate"
|
432
|
+
elsif summary[:status] == 'warning'
|
433
|
+
"#{summary[:avg_response_time]}ms avg"
|
434
|
+
else
|
435
|
+
"#{summary[:avg_response_time]}ms avg"
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
def self.cache_fresh?(cache_file_path, max_age = 5.minutes)
|
440
|
+
File.mtime(cache_file_path) > max_age.ago
|
441
|
+
rescue
|
442
|
+
false
|
443
|
+
end
|
444
|
+
|
445
|
+
def self.solidstats_cache_path(filename)
|
446
|
+
Rails.root.join('solidstats', filename)
|
447
|
+
end
|
448
|
+
|
449
|
+
def self.ensure_solidstats_directory
|
450
|
+
dir_path = File.dirname(solidstats_cache_path('dummy'))
|
451
|
+
FileUtils.mkdir_p(dir_path) unless Dir.exist?(dir_path)
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|