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