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
|